Improve data skill routing and tool organization
This commit is contained in:
+35
-8
@@ -10,10 +10,14 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
RUN_DIR="${ROOT_DIR}/.port_sessions"
|
RUN_DIR="${ROOT_DIR}/.port_sessions"
|
||||||
BACKEND_HOST="${CLAW_BACKEND_HOST:-127.0.0.1}"
|
BACKEND_HOST="${CLAW_BACKEND_HOST:-127.0.0.1}"
|
||||||
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
||||||
FRONTEND_HOST="${CLAW_FRONTEND_HOST:-127.0.0.1}"
|
FRONTEND_HOST="${CLAW_FRONTEND_HOST:-0.0.0.0}"
|
||||||
FRONTEND_PORT="${CLAW_FRONTEND_PORT:-3000}"
|
FRONTEND_PORT="${CLAW_FRONTEND_PORT:-3000}"
|
||||||
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"
|
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"
|
||||||
FRONTEND_URL="http://${FRONTEND_HOST}:${FRONTEND_PORT}"
|
FRONTEND_URL="http://${FRONTEND_HOST}:${FRONTEND_PORT}"
|
||||||
|
PYTHON_BIN="${CLAW_PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}"
|
||||||
|
if [[ ! -x "${PYTHON_BIN}" ]]; then
|
||||||
|
PYTHON_BIN="python3"
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${RUN_DIR}"
|
mkdir -p "${RUN_DIR}"
|
||||||
|
|
||||||
@@ -41,6 +45,31 @@ stop_pid_file() {
|
|||||||
rm -f "${pid_file}"
|
rm -f "${pid_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop_port_listener() {
|
||||||
|
local port="$1"
|
||||||
|
local name="$2"
|
||||||
|
local pids
|
||||||
|
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
||||||
|
if [[ -z "${pids}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "Stopping ${name} listener(s) on port ${port}: ${pids//$'\n'/ }..."
|
||||||
|
while IFS= read -r pid; do
|
||||||
|
[[ -z "${pid}" ]] && continue
|
||||||
|
kill "${pid}" 2>/dev/null || true
|
||||||
|
done <<<"${pids}"
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if ! lsof -tiTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
while IFS= read -r pid; do
|
||||||
|
[[ -z "${pid}" ]] && continue
|
||||||
|
kill -9 "${pid}" 2>/dev/null || true
|
||||||
|
done <<<"${pids}"
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_url() {
|
wait_for_url() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local name="$2"
|
local name="$2"
|
||||||
@@ -60,6 +89,8 @@ wait_for_url() {
|
|||||||
|
|
||||||
stop_pid_file "${RUN_DIR}/webui-frontend.pid" "frontend"
|
stop_pid_file "${RUN_DIR}/webui-frontend.pid" "frontend"
|
||||||
stop_pid_file "${RUN_DIR}/webui-backend.pid" "backend"
|
stop_pid_file "${RUN_DIR}/webui-backend.pid" "backend"
|
||||||
|
stop_port_listener "${FRONTEND_PORT}" "frontend"
|
||||||
|
stop_port_listener "${BACKEND_PORT}" "backend"
|
||||||
|
|
||||||
export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-Jxoh5lf1jWg6HQZYYyfyagXudGxUXNAgkZklxMnnXHn7pFmU}"
|
export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-Jxoh5lf1jWg6HQZYYyfyagXudGxUXNAgkZklxMnnXHn7pFmU}"
|
||||||
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
|
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
|
||||||
@@ -69,27 +100,23 @@ BACKEND_LOG="${RUN_DIR}/webui-backend.log"
|
|||||||
FRONTEND_LOG="${RUN_DIR}/webui-frontend.log"
|
FRONTEND_LOG="${RUN_DIR}/webui-frontend.log"
|
||||||
|
|
||||||
echo "Starting backend on ${BACKEND_URL}..."
|
echo "Starting backend on ${BACKEND_URL}..."
|
||||||
(
|
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
exec python3 -m src.gui \
|
nohup "${PYTHON_BIN}" -m src.gui \
|
||||||
--host "${BACKEND_HOST}" \
|
--host "${BACKEND_HOST}" \
|
||||||
--port "${BACKEND_PORT}" \
|
--port "${BACKEND_PORT}" \
|
||||||
--cwd "${ROOT_DIR}" \
|
--cwd "${ROOT_DIR}" \
|
||||||
--session-dir "${RUN_DIR}/agent" \
|
--session-dir "${RUN_DIR}/agent" \
|
||||||
--allow-write \
|
--allow-write \
|
||||||
--allow-shell \
|
--allow-shell \
|
||||||
--no-browser
|
--no-browser >"${BACKEND_LOG}" 2>&1 &
|
||||||
) >"${BACKEND_LOG}" 2>&1 &
|
|
||||||
echo "$!" >"${RUN_DIR}/webui-backend.pid"
|
echo "$!" >"${RUN_DIR}/webui-backend.pid"
|
||||||
|
|
||||||
wait_for_url "${BACKEND_URL}/api/state" "backend" "${BACKEND_LOG}"
|
wait_for_url "${BACKEND_URL}/api/state" "backend" "${BACKEND_LOG}"
|
||||||
|
|
||||||
echo "Starting frontend on ${FRONTEND_URL}..."
|
echo "Starting frontend on ${FRONTEND_URL}..."
|
||||||
(
|
|
||||||
cd "${ROOT_DIR}/frontend/app"
|
cd "${ROOT_DIR}/frontend/app"
|
||||||
export CLAW_API_URL="${BACKEND_URL}"
|
export CLAW_API_URL="${BACKEND_URL}"
|
||||||
exec npm run dev -- --hostname "${FRONTEND_HOST}" --port "${FRONTEND_PORT}"
|
nohup npm run dev -- --hostname "${FRONTEND_HOST}" --port "${FRONTEND_PORT}" >"${FRONTEND_LOG}" 2>&1 &
|
||||||
) >"${FRONTEND_LOG}" 2>&1 &
|
|
||||||
echo "$!" >"${RUN_DIR}/webui-frontend.pid"
|
echo "$!" >"${RUN_DIR}/webui-frontend.pid"
|
||||||
|
|
||||||
wait_for_url "${FRONTEND_URL}" "frontend" "${FRONTEND_LOG}"
|
wait_for_url "${FRONTEND_URL}" "frontend" "${FRONTEND_LOG}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from .agent_context import build_context_snapshot
|
from .agent_context import build_context_snapshot
|
||||||
from .agent_tools import AgentTool
|
from .agent_tools import AgentTool
|
||||||
from .agent_types import AgentRuntimeConfig, ModelConfig
|
from .agent_types import AgentRuntimeConfig, ModelConfig
|
||||||
|
from .bundled_skills import format_skills_for_system_prompt
|
||||||
from .builtin_agents import AgentDefinition, format_agent_listing
|
from .builtin_agents import AgentDefinition, format_agent_listing
|
||||||
|
|
||||||
SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
|
SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
|
||||||
@@ -98,6 +99,7 @@ def build_system_prompt_parts(
|
|||||||
get_doing_tasks_section(),
|
get_doing_tasks_section(),
|
||||||
get_actions_section(),
|
get_actions_section(),
|
||||||
get_using_your_tools_section(enabled_tool_names),
|
get_using_your_tools_section(enabled_tool_names),
|
||||||
|
get_skill_guidance_section(prompt_context, enabled_tool_names),
|
||||||
get_agent_guidance_section(enabled_tool_names, available_agents),
|
get_agent_guidance_section(enabled_tool_names, available_agents),
|
||||||
get_plugin_guidance_section(prompt_context),
|
get_plugin_guidance_section(prompt_context),
|
||||||
get_mcp_guidance_section(prompt_context),
|
get_mcp_guidance_section(prompt_context),
|
||||||
@@ -236,6 +238,22 @@ def get_using_your_tools_section(enabled_tool_names: set[str]) -> str:
|
|||||||
return '\n'.join(['# 使用工具', *prepend_bullets(items)])
|
return '\n'.join(['# 使用工具', *prepend_bullets(items)])
|
||||||
|
|
||||||
|
|
||||||
|
def get_skill_guidance_section(
|
||||||
|
prompt_context: PromptContext,
|
||||||
|
enabled_tool_names: set[str],
|
||||||
|
) -> str:
|
||||||
|
if 'Skill' not in enabled_tool_names:
|
||||||
|
return ''
|
||||||
|
skill_listing = format_skills_for_system_prompt(cwd=prompt_context.cwd)
|
||||||
|
items = [
|
||||||
|
'当用户请求符合某个 skill 的适用范围时,优先调用 Skill 工具进入该 skill,不要先用 Agent 子任务或通用搜索绕路。',
|
||||||
|
'数据生成、标签边界、产品定义、示例 query 到数据集这类任务,优先使用 product-data skill。',
|
||||||
|
'线上 badcase 挖掘、router session 检索、候选转样本这类任务,优先使用 online-mining skill。',
|
||||||
|
'调用 skill 后遵守 skill 内部的人类 review 门禁和允许工具列表。',
|
||||||
|
]
|
||||||
|
return '\n'.join(['# Skills', *prepend_bullets(items), '', skill_listing])
|
||||||
|
|
||||||
|
|
||||||
def get_tone_and_style_section() -> str:
|
def get_tone_and_style_section() -> str:
|
||||||
items = [
|
items = [
|
||||||
'回复保持简洁直接。',
|
'回复保持简洁直接。',
|
||||||
|
|||||||
+94
-61
@@ -2012,78 +2012,96 @@ class LocalCodingAgent:
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
def _format_generation_goal_review(self, goal: dict[str, object]) -> str:
|
def _format_generation_goal_review(self, goal: dict[str, object]) -> str:
|
||||||
|
target_summary = self._format_review_targets(goal)
|
||||||
lines = [
|
lines = [
|
||||||
'已生成待 review 的数据生成目标,本轮已暂停。',
|
'我先把生成目标整理好了,先确认边界,暂时不生成数据。',
|
||||||
'',
|
f"- 数据集:{goal.get('dataset_label', '')}",
|
||||||
f"- goal_id: `{goal.get('goal_id', '')}`",
|
|
||||||
f"- revision: `{goal.get('revision', '')}`",
|
|
||||||
f"- dataset_label: {goal.get('dataset_label', '')}",
|
|
||||||
f"- 目标摘要: {goal.get('goal_summary', '')}",
|
|
||||||
]
|
]
|
||||||
targets = goal.get('target_definitions')
|
if target_summary:
|
||||||
if isinstance(targets, list) and targets:
|
lines.append(f'- 标签:{target_summary}')
|
||||||
lines.append('- target_definitions:')
|
if goal.get('goal_summary'):
|
||||||
for item in targets:
|
lines.append(f"- 边界:{self._short_review_text(goal.get('goal_summary'))}")
|
||||||
if isinstance(item, dict):
|
scope_parts = []
|
||||||
lines.append(
|
if goal.get('coverage'):
|
||||||
f" - {item.get('name', '')}: `{item.get('target', '')}`;规则:{item.get('rule', '')}"
|
scope_parts.append(f"覆盖:{self._short_review_text(goal.get('coverage'), limit=70)}")
|
||||||
)
|
if goal.get('exclusions'):
|
||||||
elif goal.get('target'):
|
scope_parts.append(f"排除:{self._short_review_text(goal.get('exclusions'), limit=50)}")
|
||||||
lines.append(f"- target: `{goal.get('target')}`")
|
if scope_parts:
|
||||||
if goal.get('plan_hint'):
|
lines.append('- 范围:' + ';'.join(scope_parts))
|
||||||
lines.append(f"- 计划提示: {goal.get('plan_hint')}")
|
|
||||||
open_questions = goal.get('open_questions')
|
open_questions = goal.get('open_questions')
|
||||||
if isinstance(open_questions, list) and open_questions:
|
if isinstance(open_questions, list) and open_questions:
|
||||||
lines.append('- 待确认问题:')
|
questions = ';'.join(str(item).strip() for item in open_questions if isinstance(item, str) and item.strip())
|
||||||
for item in open_questions:
|
if questions:
|
||||||
if isinstance(item, str) and item:
|
lines.append(f'- 待确认:{self._short_review_text(questions)}')
|
||||||
lines.append(f' - {item}')
|
elif goal.get('plan_hint'):
|
||||||
source_refs = goal.get('source_refs')
|
lines.append(f"- 下一步:{self._short_review_text(goal.get('plan_hint'))}")
|
||||||
if isinstance(source_refs, list) and source_refs:
|
lines.append(f"- 内部:goal_id `{goal.get('goal_id', '')}`,revision `{goal.get('revision', '')}`")
|
||||||
lines.append('- 来源:')
|
lines.append('')
|
||||||
for item in source_refs:
|
lines.append('你看这个目标是否准确?没问题就回“确认目标”;想改的话直接说哪里不对。')
|
||||||
if isinstance(item, str) and item:
|
return '\n'.join(lines)
|
||||||
lines.append(f' - {item}')
|
|
||||||
|
def _format_generation_plan_review(self, plan: dict[str, object]) -> str:
|
||||||
|
if not plan.get('confirmed_goal_id'):
|
||||||
|
return self._format_direct_generation_plan_review(plan)
|
||||||
|
lines = [
|
||||||
|
'目标已确认。现在只补充生成参数,标签边界沿用上一步。',
|
||||||
|
f"- 数量:{plan.get('total_count', '')} 条",
|
||||||
|
f"- 轮次:{plan.get('turn_mix', '')}",
|
||||||
|
f"- 覆盖补充:{self._short_review_text(plan.get('coverage'))}",
|
||||||
|
f"- 排除:{self._short_review_text(plan.get('exclusions'))}",
|
||||||
|
f"- 输出:{plan.get('output_path', '')}",
|
||||||
|
f"- 内部:plan_id `{plan.get('plan_id', '')}`,revision `{plan.get('revision', '')}`",
|
||||||
|
]
|
||||||
|
lines.append('')
|
||||||
|
lines.append('如果这个数量和路径可以,就回“确认,开始生成”;想调整就直接说,比如“改成 20 条,全单轮”。')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def _format_direct_generation_plan_review(self, plan: dict[str, object]) -> str:
|
||||||
|
target_summary = self._format_review_targets(plan)
|
||||||
|
lines = [
|
||||||
|
'我把目标和生成参数合成一次确认,确认后就开始生成。',
|
||||||
|
f"- 数据集:{plan.get('dataset_label', '')}",
|
||||||
|
]
|
||||||
|
if target_summary:
|
||||||
|
lines.append(f'- 标签:{target_summary}')
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
f"- 覆盖范围: {goal.get('coverage', '')}",
|
f"- 数量:{plan.get('total_count', '')} 条;轮次:{plan.get('turn_mix', '')}",
|
||||||
f"- 排除项: {goal.get('exclusions', '')}",
|
(
|
||||||
|
'- 范围:'
|
||||||
|
f"覆盖:{self._short_review_text(plan.get('coverage'), limit=70)};"
|
||||||
|
f"排除:{self._short_review_text(plan.get('exclusions'), limit=50)}"
|
||||||
|
),
|
||||||
|
f"- 输出:{plan.get('output_path', '')}",
|
||||||
|
f"- 内部:plan_id `{plan.get('plan_id', '')}`,revision `{plan.get('revision', '')}`",
|
||||||
'',
|
'',
|
||||||
'请 review 这个生成目标:需要修改就直接回复修改意见;认可的话回复“确认目标”。',
|
'如果目标、数量和路径都可以,就回“确认,开始生成”;想调整就直接说哪里改。',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
def _format_generation_plan_review(self, plan: dict[str, object]) -> str:
|
def _format_review_targets(self, payload: dict[str, object]) -> str:
|
||||||
lines = [
|
targets = payload.get('target_definitions')
|
||||||
'已生成待 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:
|
if isinstance(targets, list) and targets:
|
||||||
lines.append('- target_definitions:')
|
parts: list[str] = []
|
||||||
for item in targets:
|
for item in targets:
|
||||||
if isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
lines.append(
|
continue
|
||||||
f" - {item.get('name', '')}: `{item.get('target', '')}`;规则:{item.get('rule', '')}"
|
name = str(item.get('name') or '').strip()
|
||||||
)
|
target = str(item.get('target') or '').strip()
|
||||||
elif plan.get('target'):
|
if name and target:
|
||||||
lines.append(f"- target: `{plan.get('target')}`")
|
parts.append(f'{name} -> `{target}`')
|
||||||
lines.extend(
|
elif target:
|
||||||
[
|
parts.append(f'`{target}`')
|
||||||
f"- 生成数量: {plan.get('total_count', '')}",
|
return ';'.join(parts)
|
||||||
f"- 单轮/多轮: {plan.get('turn_mix', '')}",
|
target = payload.get('target')
|
||||||
f"- 覆盖范围: {plan.get('coverage', '')}",
|
return f'`{target}`' if isinstance(target, str) and target.strip() else ''
|
||||||
f"- 排除项: {plan.get('exclusions', '')}",
|
|
||||||
f"- 落盘路径: {plan.get('output_path', '')}",
|
def _short_review_text(self, value: object, *, limit: int = 120) -> str:
|
||||||
'',
|
text = ' '.join(str(value or '').split())
|
||||||
'请 review 这份计划:需要修改就直接回复修改意见;认可的话回复“确认,开始生成”。',
|
if len(text) <= limit:
|
||||||
]
|
return text
|
||||||
)
|
return text[: limit - 1].rstrip() + '…'
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def _compact_prefix_count(self, session: AgentSessionState) -> int:
|
def _compact_prefix_count(self, session: AgentSessionState) -> int:
|
||||||
prefix_count = 0
|
prefix_count = 0
|
||||||
@@ -2388,14 +2406,29 @@ class LocalCodingAgent:
|
|||||||
|
|
||||||
# Explicit model param in arguments takes priority
|
# Explicit model param in arguments takes priority
|
||||||
if isinstance(model_override, str) and model_override.strip():
|
if isinstance(model_override, str) and model_override.strip():
|
||||||
return replace(self.model_config, model=model_override.strip())
|
resolved_model = self._resolve_child_model_name(model_override.strip())
|
||||||
|
return replace(self.model_config, model=resolved_model)
|
||||||
|
|
||||||
# Agent definition model
|
# Agent definition model
|
||||||
if agent_model and agent_model != 'inherit':
|
if agent_model and agent_model != 'inherit':
|
||||||
return replace(self.model_config, model=agent_model)
|
resolved_model = self._resolve_child_model_name(agent_model)
|
||||||
|
return replace(self.model_config, model=resolved_model)
|
||||||
|
|
||||||
return self.model_config
|
return self.model_config
|
||||||
|
|
||||||
|
def _resolve_child_model_name(self, requested_model: str) -> str:
|
||||||
|
"""Map Claude-style child model aliases only when the parent model is Claude-like."""
|
||||||
|
|
||||||
|
normalized = requested_model.strip()
|
||||||
|
if normalized in {'haiku', 'sonnet', 'opus'} and not self._is_claude_model(self.model_config.model):
|
||||||
|
return self.model_config.model
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_claude_model(model: str) -> bool:
|
||||||
|
lowered = model.lower()
|
||||||
|
return lowered.startswith('claude') or '/claude' in lowered or 'anthropic' in lowered
|
||||||
|
|
||||||
def _filter_tools_for_agent(
|
def _filter_tools_for_agent(
|
||||||
self,
|
self,
|
||||||
agent_def: AgentDefinition,
|
agent_def: AgentDefinition,
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Union
|
||||||
|
|
||||||
|
from .agent_types import AgentPermissions, AgentRuntimeConfig, ToolExecutionResult
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .account_runtime import AccountRuntime
|
||||||
|
from .ask_user_runtime import AskUserRuntime
|
||||||
|
from .config_runtime import ConfigRuntime
|
||||||
|
from .lsp_runtime import LSPRuntime
|
||||||
|
from .mcp_runtime import MCPRuntime
|
||||||
|
from .plan_runtime import PlanRuntime
|
||||||
|
from .remote_runtime import RemoteRuntime
|
||||||
|
from .remote_trigger_runtime import RemoteTriggerRuntime
|
||||||
|
from .search_runtime import SearchRuntime
|
||||||
|
from .task_runtime import TaskRuntime
|
||||||
|
from .team_runtime import TeamRuntime
|
||||||
|
from .workflow_runtime import WorkflowRuntime
|
||||||
|
from .worktree_runtime import WorktreeRuntime
|
||||||
|
|
||||||
|
|
||||||
|
class ToolPermissionError(RuntimeError):
|
||||||
|
"""Raised when the runtime configuration does not allow a tool action."""
|
||||||
|
|
||||||
|
|
||||||
|
class ToolExecutionError(RuntimeError):
|
||||||
|
"""Raised when a tool cannot complete because of invalid input or state."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ToolExecutionContext:
|
||||||
|
root: Path
|
||||||
|
command_timeout_seconds: float
|
||||||
|
max_output_chars: int
|
||||||
|
permissions: AgentPermissions
|
||||||
|
scratchpad_directory: Path | None = None
|
||||||
|
python_env_dir: Path | None = None
|
||||||
|
extra_env: dict[str, str] = field(default_factory=dict)
|
||||||
|
tool_registry: dict[str, 'AgentTool'] | None = None
|
||||||
|
search_runtime: 'SearchRuntime | None' = None
|
||||||
|
account_runtime: 'AccountRuntime | None' = None
|
||||||
|
ask_user_runtime: 'AskUserRuntime | None' = None
|
||||||
|
config_runtime: 'ConfigRuntime | None' = None
|
||||||
|
lsp_runtime: 'LSPRuntime | None' = None
|
||||||
|
mcp_runtime: 'MCPRuntime | None' = None
|
||||||
|
remote_runtime: 'RemoteRuntime | None' = None
|
||||||
|
remote_trigger_runtime: 'RemoteTriggerRuntime | None' = None
|
||||||
|
plan_runtime: 'PlanRuntime | None' = None
|
||||||
|
task_runtime: 'TaskRuntime | None' = None
|
||||||
|
team_runtime: 'TeamRuntime | None' = None
|
||||||
|
workflow_runtime: 'WorkflowRuntime | None' = None
|
||||||
|
worktree_runtime: 'WorktreeRuntime | None' = None
|
||||||
|
|
||||||
|
|
||||||
|
ToolHandler = Callable[
|
||||||
|
[dict[str, Any], ToolExecutionContext],
|
||||||
|
Union[str, tuple[str, dict[str, Any]]],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AgentTool:
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
parameters: dict[str, Any]
|
||||||
|
handler: ToolHandler
|
||||||
|
|
||||||
|
def to_openai_tool(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
'type': 'function',
|
||||||
|
'function': {
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'parameters': self.parameters,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute(
|
||||||
|
self,
|
||||||
|
arguments: dict[str, Any],
|
||||||
|
context: ToolExecutionContext,
|
||||||
|
) -> ToolExecutionResult:
|
||||||
|
try:
|
||||||
|
result = self.handler(arguments, context)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
content, metadata = result
|
||||||
|
else:
|
||||||
|
content, metadata = result, {}
|
||||||
|
return ToolExecutionResult(
|
||||||
|
name=self.name,
|
||||||
|
ok=True,
|
||||||
|
content=content,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
except ToolPermissionError as exc:
|
||||||
|
return ToolExecutionResult(
|
||||||
|
name=self.name,
|
||||||
|
ok=False,
|
||||||
|
content=str(exc),
|
||||||
|
metadata={'error_kind': 'permission_denied'},
|
||||||
|
)
|
||||||
|
except (ToolExecutionError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
return ToolExecutionResult(
|
||||||
|
name=self.name,
|
||||||
|
ok=False,
|
||||||
|
content=str(exc),
|
||||||
|
metadata={'error_kind': 'tool_execution_error'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ToolStreamUpdate:
|
||||||
|
kind: str
|
||||||
|
content: str = ''
|
||||||
|
stream: str | None = None
|
||||||
|
result: ToolExecutionResult | None = None
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def build_tool_context(
|
||||||
|
config: AgentRuntimeConfig,
|
||||||
|
*,
|
||||||
|
scratchpad_directory: Path | None = None,
|
||||||
|
python_env_dir: Path | None = None,
|
||||||
|
extra_env: dict[str, str] | None = None,
|
||||||
|
tool_registry: dict[str, AgentTool] | None = None,
|
||||||
|
search_runtime: 'SearchRuntime | None' = None,
|
||||||
|
account_runtime: 'AccountRuntime | None' = None,
|
||||||
|
ask_user_runtime: 'AskUserRuntime | None' = None,
|
||||||
|
config_runtime: 'ConfigRuntime | None' = None,
|
||||||
|
lsp_runtime: 'LSPRuntime | None' = None,
|
||||||
|
mcp_runtime: 'MCPRuntime | None' = None,
|
||||||
|
remote_runtime: 'RemoteRuntime | None' = None,
|
||||||
|
remote_trigger_runtime: 'RemoteTriggerRuntime | None' = None,
|
||||||
|
plan_runtime: 'PlanRuntime | None' = None,
|
||||||
|
task_runtime: 'TaskRuntime | None' = None,
|
||||||
|
team_runtime: 'TeamRuntime | None' = None,
|
||||||
|
workflow_runtime: 'WorkflowRuntime | None' = None,
|
||||||
|
worktree_runtime: 'WorktreeRuntime | None' = None,
|
||||||
|
) -> ToolExecutionContext:
|
||||||
|
return ToolExecutionContext(
|
||||||
|
root=config.cwd.resolve(),
|
||||||
|
command_timeout_seconds=config.command_timeout_seconds,
|
||||||
|
max_output_chars=config.max_output_chars,
|
||||||
|
permissions=config.permissions,
|
||||||
|
scratchpad_directory=scratchpad_directory.resolve() if scratchpad_directory else None,
|
||||||
|
python_env_dir=(
|
||||||
|
python_env_dir.resolve()
|
||||||
|
if python_env_dir
|
||||||
|
else config.python_env_dir.resolve()
|
||||||
|
if config.python_env_dir
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
extra_env=dict(extra_env or {}),
|
||||||
|
tool_registry=tool_registry,
|
||||||
|
search_runtime=search_runtime,
|
||||||
|
account_runtime=account_runtime,
|
||||||
|
ask_user_runtime=ask_user_runtime,
|
||||||
|
config_runtime=config_runtime,
|
||||||
|
lsp_runtime=lsp_runtime,
|
||||||
|
mcp_runtime=mcp_runtime,
|
||||||
|
remote_runtime=remote_runtime,
|
||||||
|
remote_trigger_runtime=remote_trigger_runtime,
|
||||||
|
plan_runtime=plan_runtime,
|
||||||
|
task_runtime=task_runtime,
|
||||||
|
team_runtime=team_runtime,
|
||||||
|
workflow_runtime=workflow_runtime,
|
||||||
|
worktree_runtime=worktree_runtime,
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""工具声明目录。
|
||||||
|
|
||||||
|
每个子模块负责一类工具的名称、描述和 JSON Schema;handler 由运行层注入。
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from ..agent_tool_core import ToolHandler
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_handler(handlers: Mapping[str, ToolHandler], name: str, group: str) -> ToolHandler:
|
||||||
|
"""从工具组 handler 映射中取出指定工具的执行函数。"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return handlers[name]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise KeyError(f'Missing {group} tool handler: {name}') from exc
|
||||||
|
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from ..agent_tool_core import AgentTool, ToolHandler
|
||||||
|
from ._builder import resolve_handler
|
||||||
|
|
||||||
|
|
||||||
|
def build_data_agent_tools(handlers: Mapping[str, ToolHandler]) -> list[AgentTool]:
|
||||||
|
"""构建数据 Agent 专用工具声明。
|
||||||
|
|
||||||
|
这里仅保存工具名称、描述和 JSON Schema,具体执行逻辑由调用方传入的 handler 提供。
|
||||||
|
这样新增数据 Agent 工具时,可以优先在本文件补充声明,再在实现模块中补充 handler。
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_prepare_generation_goal',
|
||||||
|
description=(
|
||||||
|
'Create a pending data-agent generation goal from source evidence or user rules. '
|
||||||
|
'Use this before data_agent_prepare_generation_plan; show the returned goal to the user and wait for confirmation.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'dataset_label': {'type': 'string'},
|
||||||
|
'goal_summary': {'type': 'string'},
|
||||||
|
'target': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Single full target expression, e.g. Agent(tag="餐饮服务") or a function call. For multi-target boundary tasks, use target_definitions.',
|
||||||
|
},
|
||||||
|
'target_definitions': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'target': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Full target expression, e.g. Agent(tag="餐饮服务"); do not shorten to a display name.',
|
||||||
|
},
|
||||||
|
'rule': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['target'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'plan_hint': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': (
|
||||||
|
'Optional plain-language hint for the later plan, such as '
|
||||||
|
'"建议先生成 50 条单轮,输出到 tasks/.../records.jsonl". '
|
||||||
|
'Structured total_count, turn_mix, and output_path belong to data_agent_prepare_generation_plan.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'coverage': {'type': 'string'},
|
||||||
|
'exclusions': {'type': 'string'},
|
||||||
|
'open_questions': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'source_refs': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'notes': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['dataset_label', 'goal_summary', 'coverage', 'exclusions'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_prepare_generation_goal', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_confirm_generation_goal',
|
||||||
|
description=(
|
||||||
|
'Mark a pending data-agent generation goal as confirmed after the user explicitly approves it. '
|
||||||
|
'The returned confirmed_goal_id is required by data_agent_prepare_generation_plan.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'goal_id': {'type': 'string'},
|
||||||
|
'confirmation': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The user approval text, such as 确认, 开始生成, or approve.',
|
||||||
|
},
|
||||||
|
'reviewed_revision': {
|
||||||
|
'type': 'integer',
|
||||||
|
'description': 'The goal revision shown to the user before confirmation.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['goal_id', 'confirmation'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_confirm_generation_goal', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_prepare_generation_plan',
|
||||||
|
description=(
|
||||||
|
'Create a pending data-agent generation plan. Use this before generating dataset draft text; '
|
||||||
|
'requires confirmed_goal_id from data_agent_confirm_generation_goal; show the returned plan to the user and wait for confirmation.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'confirmed_goal_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Returned by data_agent_confirm_generation_goal after the user reviews a separate generation goal. Omit only when direct_review is true.',
|
||||||
|
},
|
||||||
|
'direct_review': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': (
|
||||||
|
'Set true for simple hand-written rules where the user already supplied target labels '
|
||||||
|
'and wants generation; this creates one combined goal+plan review instead of a separate goal review.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'dataset_label': {'type': 'string'},
|
||||||
|
'target': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Single full target expression. For multi-target boundary tasks, omit this and fill target_definitions.',
|
||||||
|
},
|
||||||
|
'target_definitions': {
|
||||||
|
'type': 'array',
|
||||||
|
'description': 'Optional target labels for multi-class boundary data.',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'target': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Full target expression inherited from the confirmed goal, e.g. Agent(tag="餐饮服务").',
|
||||||
|
},
|
||||||
|
'rule': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['target'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'total_count': {'type': 'integer', 'minimum': 1},
|
||||||
|
'turn_mix': {'type': 'string', 'description': 'Single-turn and multi-turn count or ratio.'},
|
||||||
|
'coverage': {'type': 'string', 'description': 'Query types, intent boundaries, or error types to cover.'},
|
||||||
|
'exclusions': {'type': 'string', 'description': 'Negative examples or boundaries to avoid.'},
|
||||||
|
'output_path': {'type': 'string', 'description': 'Where draft, records, and validation files should be written.'},
|
||||||
|
'notes': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': [
|
||||||
|
'dataset_label',
|
||||||
|
'total_count',
|
||||||
|
'turn_mix',
|
||||||
|
'coverage',
|
||||||
|
'exclusions',
|
||||||
|
'output_path',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_prepare_generation_plan', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_load_input_sources',
|
||||||
|
description=(
|
||||||
|
'Load data-agent input files or directories and extract structured paragraphs and table previews '
|
||||||
|
'from xlsx, csv, docx, pdf, txt, md, json, or jsonl sources.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'paths': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Workspace-relative files or directories to load.',
|
||||||
|
},
|
||||||
|
'max_files': {'type': 'integer', 'minimum': 1, 'maximum': 100},
|
||||||
|
'max_paragraphs_per_file': {'type': 'integer', 'minimum': 1, 'maximum': 300},
|
||||||
|
'max_tables_per_file': {'type': 'integer', 'minimum': 1, 'maximum': 100},
|
||||||
|
'max_rows_per_table': {'type': 'integer', 'minimum': 1, 'maximum': 200},
|
||||||
|
'max_cell_chars': {'type': 'integer', 'minimum': 20, 'maximum': 2000},
|
||||||
|
},
|
||||||
|
'required': ['paths'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_load_input_sources', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_extract_case_evidence',
|
||||||
|
description=(
|
||||||
|
'Extract query/badcase evidence from loaded input sources or source paths. '
|
||||||
|
'Profiles table columns first and returns required questions when query or expected label columns are ambiguous.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'paths': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Workspace-relative files or directories. Omit when loaded_sources is provided.',
|
||||||
|
},
|
||||||
|
'loaded_sources': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Output from data_agent_load_input_sources.',
|
||||||
|
},
|
||||||
|
'field_mapping': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Optional role-to-column mapping, e.g. {"query":"query","expected_label":"预期domain"}.',
|
||||||
|
},
|
||||||
|
'max_cases': {'type': 'integer', 'minimum': 1, 'maximum': 1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_extract_case_evidence', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_render_source_context',
|
||||||
|
description=(
|
||||||
|
'Render loaded data-agent sources into LLM-readable evidence text with source refs. '
|
||||||
|
'Use this after data_agent_load_input_sources before asking the model to structure product semantics.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'paths': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Workspace-relative files or directories. Omit when loaded_sources is provided.',
|
||||||
|
},
|
||||||
|
'loaded_sources': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Output from data_agent_load_input_sources.',
|
||||||
|
},
|
||||||
|
'max_chars': {'type': 'integer', 'minimum': 1000, 'maximum': 200000},
|
||||||
|
'max_tables_per_source': {'type': 'integer', 'minimum': 1, 'maximum': 100},
|
||||||
|
'max_rows_per_table': {'type': 'integer', 'minimum': 1, 'maximum': 200},
|
||||||
|
'focus_keywords': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Optional keywords used to keep only matching paragraphs/table rows.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_render_source_context', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_profile_router_sessions',
|
||||||
|
description=(
|
||||||
|
'Profile local router_session_parquet date partitions before mining online sessions. '
|
||||||
|
'Returns schema, sampled row count, distributions, and example sessions.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'dates': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Date partitions such as ["20260428"]. Resolved under router_session_parquet/date=YYYYMMDD.',
|
||||||
|
},
|
||||||
|
'paths': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Workspace-relative parquet files or date directories. Use when dates is not enough.',
|
||||||
|
},
|
||||||
|
'max_files': {'type': 'integer', 'minimum': 1, 'maximum': 200},
|
||||||
|
'max_rows_per_file': {'type': 'integer', 'minimum': 1, 'maximum': 100000},
|
||||||
|
'examples_limit': {'type': 'integer', 'minimum': 0, 'maximum': 20},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_profile_router_sessions', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_search_router_sessions',
|
||||||
|
description=(
|
||||||
|
'Search local router_session_parquet data with reviewable filters such as device, domain, intent, func, '
|
||||||
|
'query keywords, regex, turn count, and match scope. Returns turn-level candidates with prev turns.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'dates': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Date partitions such as ["20260428"]. Resolved under router_session_parquet/date=YYYYMMDD.',
|
||||||
|
},
|
||||||
|
'paths': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': 'Workspace-relative parquet files or date directories. Use when dates is not enough.',
|
||||||
|
},
|
||||||
|
'devices': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'domains': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'intents': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'funcs': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'query_keywords': {'type': 'array', 'items': {'type': 'string'}},
|
||||||
|
'keyword_match_mode': {'type': 'string', 'enum': ['any', 'all']},
|
||||||
|
'query_regex': {'type': 'string'},
|
||||||
|
'match_scope': {'type': 'string', 'enum': ['any_turn', 'last_turn']},
|
||||||
|
'turn_count_min': {'type': 'integer', 'minimum': 1},
|
||||||
|
'turn_count_max': {'type': 'integer', 'minimum': 1},
|
||||||
|
'max_files': {'type': 'integer', 'minimum': 1, 'maximum': 200},
|
||||||
|
'max_rows_per_file': {'type': 'integer', 'minimum': 1, 'maximum': 100000},
|
||||||
|
'max_candidates': {'type': 'integer', 'minimum': 1, 'maximum': 5000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_search_router_sessions', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_sample_router_candidates',
|
||||||
|
description=(
|
||||||
|
'Sample router session candidates for human review and return candidate-level summary statistics. '
|
||||||
|
'Use after data_agent_search_router_sessions.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'candidates': {
|
||||||
|
'oneOf': [
|
||||||
|
{'type': 'array'},
|
||||||
|
{'type': 'object'},
|
||||||
|
{'type': 'string'},
|
||||||
|
],
|
||||||
|
'description': 'Candidates array, search result object, or JSON string containing candidates.',
|
||||||
|
},
|
||||||
|
'sample_size': {'type': 'integer', 'minimum': 1, 'maximum': 1000},
|
||||||
|
'strategy': {'type': 'string', 'enum': ['random', 'first', 'stride']},
|
||||||
|
'seed': {'type': 'integer'},
|
||||||
|
},
|
||||||
|
'required': ['candidates'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_sample_router_candidates', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_convert_router_candidates_to_records',
|
||||||
|
description=(
|
||||||
|
'Convert reviewed online router session candidates directly into canonical data-agent records. '
|
||||||
|
'Use this when the user wants to keep mined online data as samples; do not use generation-plan tools for this path.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'candidates': {
|
||||||
|
'oneOf': [
|
||||||
|
{'type': 'array'},
|
||||||
|
{'type': 'object'},
|
||||||
|
{'type': 'string'},
|
||||||
|
],
|
||||||
|
'description': 'Candidates array, search/sample result object, or JSON string containing candidates.',
|
||||||
|
},
|
||||||
|
'dataset_label': {'type': 'string'},
|
||||||
|
'default_target': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Target label used for included candidates unless review_decisions provides a target.',
|
||||||
|
},
|
||||||
|
'review_decisions': {
|
||||||
|
'type': 'array',
|
||||||
|
'description': 'Optional include/exclude/uncertain decisions keyed by semantic_session_id or req_id.',
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'semantic_session_id': {'type': 'string'},
|
||||||
|
'req_id': {'type': 'string'},
|
||||||
|
'matched_turn_index': {'type': 'integer'},
|
||||||
|
'decision': {'type': 'string', 'enum': ['include', 'exclude', 'uncertain']},
|
||||||
|
'target': {'type': 'string'},
|
||||||
|
'notes': {'type': 'string'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'batch_id': {'type': 'string'},
|
||||||
|
'include_uncertain': {'type': 'boolean'},
|
||||||
|
},
|
||||||
|
'required': ['candidates', 'dataset_label'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_convert_router_candidates_to_records', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_show_generation_plan',
|
||||||
|
description='Show a data-agent generation plan for human review.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'plan_id': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['plan_id'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_show_generation_plan', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_update_generation_plan',
|
||||||
|
description=(
|
||||||
|
'Update a pending data-agent generation plan from human review feedback, then show it again for review.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'plan_id': {'type': 'string'},
|
||||||
|
'review_feedback': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Human review feedback that explains why the plan is changing.',
|
||||||
|
},
|
||||||
|
'updates': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Fields to update on the plan.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['plan_id', 'review_feedback', 'updates'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_update_generation_plan', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_confirm_generation_plan',
|
||||||
|
description=(
|
||||||
|
'Mark a pending data-agent generation plan as confirmed after the user explicitly approves it.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'plan_id': {'type': 'string'},
|
||||||
|
'confirmation': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The user approval text, such as 确认, 开始生成, or approve.',
|
||||||
|
},
|
||||||
|
'reviewed_revision': {
|
||||||
|
'type': 'integer',
|
||||||
|
'description': 'The plan revision shown to the user before confirmation.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['plan_id', 'confirmation'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_confirm_generation_plan', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_normalize_dataset_draft',
|
||||||
|
description=(
|
||||||
|
'Parse dataset draft text written with 用户/小爱/target blocks and build canonical '
|
||||||
|
'data-agent records with generated record IDs, timestamps, source metadata, context, and labels. '
|
||||||
|
'Generated data requires a confirmed_plan_id from data_agent_confirm_generation_plan.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'draft_text': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Dataset draft text in dataset draft text v1 format.',
|
||||||
|
},
|
||||||
|
'batch_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Batch identifier used in generated record_id values.',
|
||||||
|
},
|
||||||
|
'source_type': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['generated', 'online', 'manual', 'mixed'],
|
||||||
|
},
|
||||||
|
'base_timestamp': {
|
||||||
|
'type': 'integer',
|
||||||
|
'description': 'Optional millisecond timestamp for deterministic generated records.',
|
||||||
|
},
|
||||||
|
'timestamp_step_ms': {
|
||||||
|
'type': 'integer',
|
||||||
|
'minimum': 1,
|
||||||
|
'description': 'Millisecond gap between adjacent turns. Defaults to 60000.',
|
||||||
|
},
|
||||||
|
'default_request_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Request ID to use when a case does not provide a real request_id.',
|
||||||
|
},
|
||||||
|
'confirmed_plan_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Required for generated data; returned by data_agent_confirm_generation_plan.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['draft_text'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_normalize_dataset_draft', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_validate_dataset_records',
|
||||||
|
description=(
|
||||||
|
'Validate canonical data-agent records for required fields, labels, source metadata, '
|
||||||
|
'prev_session structure, timestamp order, and 5-minute prompt stitching constraints.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'records': {
|
||||||
|
'oneOf': [
|
||||||
|
{'type': 'array'},
|
||||||
|
{'type': 'string'},
|
||||||
|
],
|
||||||
|
'description': 'Canonical records as an array, or a JSON string containing the array.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['records'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_validate_dataset_records', 'data-agent'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='data_agent_export_dataset_records',
|
||||||
|
description=(
|
||||||
|
'Validate canonical data-agent records and write them to a workspace file. '
|
||||||
|
'Defaults to compact JSONL, one canonical record per line, so the model does not hand-write JSON output.'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'records': {
|
||||||
|
'oneOf': [
|
||||||
|
{'type': 'array'},
|
||||||
|
{'type': 'string'},
|
||||||
|
],
|
||||||
|
'description': 'Canonical records as an array, or a JSON string containing the array.',
|
||||||
|
},
|
||||||
|
'output_path': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Workspace-relative path to write, usually ending in .jsonl.',
|
||||||
|
},
|
||||||
|
'output_format': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['jsonl', 'json'],
|
||||||
|
'description': 'Defaults to jsonl. json writes a compact JSON array.',
|
||||||
|
},
|
||||||
|
'require_validation_ok': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'When true, refuse to write records with validation errors.',
|
||||||
|
},
|
||||||
|
'overwrite': {
|
||||||
|
'type': 'boolean',
|
||||||
|
'description': 'When false, refuse to overwrite an existing file.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['records', 'output_path'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'data_agent_export_dataset_records', 'data-agent'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from ..agent_tool_core import AgentTool, ToolHandler
|
||||||
|
from ._builder import resolve_handler
|
||||||
|
|
||||||
|
|
||||||
|
def build_execution_tools(handlers: Mapping[str, ToolHandler]) -> list[AgentTool]:
|
||||||
|
"""构建本地命令、Python 执行与短等待工具声明。"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
AgentTool(
|
||||||
|
name='python_exec',
|
||||||
|
description=(
|
||||||
|
'优先用本工具执行小型 Python 代码或项目内 Python 脚本,用于结构化文件分析、'
|
||||||
|
'JSON/JSONL 处理、批量校验、数据抽样和快速计算。默认使用当前用户独立 Python venv;'
|
||||||
|
'不要用 bash 运行 python/python3/.venv/bin/python。不要用本工具安装依赖。'
|
||||||
|
'缺包时应向用户确认后再处理依赖。一次性分析优先传 code;如需写临时文件,'
|
||||||
|
'必须写入环境变量 PYTHON_EXEC_SCRATCHPAD 指向的会话隔离目录,'
|
||||||
|
'不要在项目根目录创建临时 .py 脚本。'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'code': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '要通过 python -c 执行的 Python 代码。code 和 script_path 必须二选一;一次性分析优先使用 code。',
|
||||||
|
},
|
||||||
|
'script_path': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '工作区内已有、需要长期复用的 Python 脚本路径。code 和 script_path 必须二选一;不要为一次性分析在项目根目录新建脚本。',
|
||||||
|
},
|
||||||
|
'args': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': '传给脚本的命令行参数。仅在 script_path 模式下使用。',
|
||||||
|
},
|
||||||
|
'stdin': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': '可选标准输入内容。',
|
||||||
|
},
|
||||||
|
'timeout_seconds': {
|
||||||
|
'type': 'number',
|
||||||
|
'minimum': 1,
|
||||||
|
'description': '可选超时时间,默认使用当前会话命令超时。',
|
||||||
|
},
|
||||||
|
'max_output_chars': {
|
||||||
|
'type': 'integer',
|
||||||
|
'minimum': 100,
|
||||||
|
'description': '可选输出截断长度,默认使用当前会话输出限制。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'python_exec', 'execution'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='python_package',
|
||||||
|
description=(
|
||||||
|
'在当前用户独立 Python venv 中检查或安装 Python 包。'
|
||||||
|
'用于 python_exec 缺少 pandas、pyarrow、openpyxl 等分析依赖时;'
|
||||||
|
'不要通过 bash 执行 pip,也不要安装到项目 .venv。'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'action': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['show', 'install'],
|
||||||
|
'description': 'show 查看当前 Python 环境和 pip;install 安装 packages。',
|
||||||
|
},
|
||||||
|
'packages': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'description': '要安装的包名或 pip requirement spec,例如 pandas、pyarrow==15.0.0。',
|
||||||
|
},
|
||||||
|
'timeout_seconds': {
|
||||||
|
'type': 'number',
|
||||||
|
'minimum': 1,
|
||||||
|
'maximum': 600,
|
||||||
|
'description': '安装超时时间,默认 120 秒。',
|
||||||
|
},
|
||||||
|
'max_output_chars': {
|
||||||
|
'type': 'integer',
|
||||||
|
'minimum': 1000,
|
||||||
|
'maximum': 50000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['action'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'python_package', 'execution'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='bash',
|
||||||
|
description=(
|
||||||
|
'运行真实 shell 命令,例如系统命令、进程控制、git 只读检查或用户已确认的依赖安装。'
|
||||||
|
'不要用 bash 执行 python、python3、pip 或 .venv/bin/python;结构化文件分析、'
|
||||||
|
'JSON/JSONL 处理、批量校验、数据抽样和快速计算必须优先使用 python_exec。'
|
||||||
|
'Python 包安装必须优先使用 python_package。'
|
||||||
|
),
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'command': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['command'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'bash', 'execution'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='sleep',
|
||||||
|
description='Pause execution briefly for bounded local wait flows.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'seconds': {'type': 'number', 'minimum': 0.0, 'maximum': 5.0},
|
||||||
|
},
|
||||||
|
'required': ['seconds'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'sleep', 'execution'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from ..agent_tool_core import AgentTool, ToolHandler
|
||||||
|
from ._builder import resolve_handler
|
||||||
|
|
||||||
|
|
||||||
|
def build_file_tools(handlers: Mapping[str, ToolHandler]) -> list[AgentTool]:
|
||||||
|
"""构建工作区文件读写与文本检索工具声明。"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
AgentTool(
|
||||||
|
name='list_dir',
|
||||||
|
description='List files and directories under a workspace path.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'path': {'type': 'string', 'description': 'Relative path from workspace root.'},
|
||||||
|
'max_entries': {'type': 'integer', 'minimum': 1, 'maximum': 500},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'list_dir', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='read_file',
|
||||||
|
description='Read the contents of a UTF-8 text file inside the workspace.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'path': {'type': 'string', 'description': 'Relative file path from workspace root.'},
|
||||||
|
'start_line': {'type': 'integer', 'minimum': 1},
|
||||||
|
'end_line': {'type': 'integer', 'minimum': 1},
|
||||||
|
},
|
||||||
|
'required': ['path'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'read_file', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='write_file',
|
||||||
|
description='Write a complete file inside the workspace. Creates parent directories when needed.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'path': {'type': 'string'},
|
||||||
|
'content': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['path', 'content'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'write_file', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='edit_file',
|
||||||
|
description='Replace text inside a workspace file using exact string matching.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'path': {'type': 'string'},
|
||||||
|
'old_text': {'type': 'string'},
|
||||||
|
'new_text': {'type': 'string'},
|
||||||
|
'replace_all': {'type': 'boolean'},
|
||||||
|
},
|
||||||
|
'required': ['path', 'old_text', 'new_text'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'edit_file', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='notebook_edit',
|
||||||
|
description='Edit a Jupyter notebook cell by replacing or appending source in a .ipynb file.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'path': {'type': 'string'},
|
||||||
|
'cell_index': {'type': 'integer', 'minimum': 0},
|
||||||
|
'source': {'type': 'string'},
|
||||||
|
'cell_type': {'type': 'string'},
|
||||||
|
'create_cell': {'type': 'boolean'},
|
||||||
|
},
|
||||||
|
'required': ['path', 'cell_index', 'source'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'notebook_edit', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='glob_search',
|
||||||
|
description='Find files matching a glob pattern inside the workspace.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'pattern': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['pattern'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'glob_search', 'file'),
|
||||||
|
),
|
||||||
|
AgentTool(
|
||||||
|
name='grep_search',
|
||||||
|
description='Search for a string or regular expression inside workspace files.',
|
||||||
|
parameters={
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'pattern': {'type': 'string'},
|
||||||
|
'path': {'type': 'string'},
|
||||||
|
'literal': {'type': 'boolean'},
|
||||||
|
'max_matches': {'type': 'integer', 'minimum': 1, 'maximum': 500},
|
||||||
|
},
|
||||||
|
'required': ['pattern'],
|
||||||
|
},
|
||||||
|
handler=resolve_handler(handlers, 'grep_search', 'file'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
+80
-863
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,8 @@ def prepare_generation_goal(
|
|||||||
if missing:
|
if missing:
|
||||||
raise DataRecordError('missing required goal fields: ' + ', '.join(missing))
|
raise DataRecordError('missing required goal fields: ' + ', '.join(missing))
|
||||||
normalized_targets = _normalize_target_definitions(target_definitions)
|
normalized_targets = _normalize_target_definitions(target_definitions)
|
||||||
|
if not target.strip() and not normalized_targets:
|
||||||
|
raise DataRecordError('generation goal must include target or target_definitions')
|
||||||
state = _load_goal_state(root)
|
state = _load_goal_state(root)
|
||||||
sequence = int(state.get('next_sequence') or 1)
|
sequence = int(state.get('next_sequence') or 1)
|
||||||
goal_id = f'data_goal_{sequence:06d}'
|
goal_id = f'data_goal_{sequence:06d}'
|
||||||
@@ -186,6 +188,12 @@ def prepare_generation_plan(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
normalized_targets = _normalize_target_definitions(target_definitions)
|
normalized_targets = _normalize_target_definitions(target_definitions)
|
||||||
|
if confirmed_goal is not None and not target.strip() and not normalized_targets:
|
||||||
|
normalized_targets = _normalize_target_definitions(
|
||||||
|
confirmed_goal.get('target_definitions') if isinstance(confirmed_goal, dict) else None
|
||||||
|
)
|
||||||
|
if not normalized_targets:
|
||||||
|
target = str(confirmed_goal.get('target') or '').strip()
|
||||||
if not target.strip() and not normalized_targets:
|
if not target.strip() and not normalized_targets:
|
||||||
missing.append('target')
|
missing.append('target')
|
||||||
if missing:
|
if missing:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, ask_u
|
|||||||
|
|
||||||
使用这个 skill 处理“badcase 或标签定义 -> 挖掘策略 -> 候选召回 -> 抽样 review -> 策略迭代 -> mined dataset”的工作流。
|
使用这个 skill 处理“badcase 或标签定义 -> 挖掘策略 -> 候选召回 -> 抽样 review -> 策略迭代 -> mined dataset”的工作流。
|
||||||
|
|
||||||
|
所有线上挖掘产物必须放在当前用户当前会话的 output 目录下。不要把 records 或 review 结果写到项目根目录的 `output/`、`tasks/` 或源码目录。工具会把相对 `output_path` 自动路由到会话 output 目录;展示给用户时以工具返回的实际路径为准。
|
||||||
|
|
||||||
## 两条分支
|
## 两条分支
|
||||||
|
|
||||||
线上挖掘后必须先判断用户要的是哪条分支。
|
线上挖掘后必须先判断用户要的是哪条分支。
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ source_refs:
|
|||||||
|
|
||||||
默认不要一步到位生成数据。除非用户已经明确说“开始生成”“确认计划”“按这个计划生成”或同义表达,否则只能做目标对齐、计划草案和问题确认。
|
默认不要一步到位生成数据。除非用户已经明确说“开始生成”“确认计划”“按这个计划生成”或同义表达,否则只能做目标对齐、计划草案和问题确认。
|
||||||
|
|
||||||
|
为了减少重复确认,优先按下面两种门禁模式选择:
|
||||||
|
|
||||||
|
- **一次确认模式**:用户直接给出手写规则、完整 target 表达,并且明确希望生成数据时,直接调用 `data_agent_prepare_generation_plan`,传 `direct_review=true` 和 `target_definitions`。这一次 review 同时确认目标、数量、轮次和路径;用户回复“确认,开始生成”后即可调用 `data_agent_confirm_generation_plan`。
|
||||||
|
- **两段确认模式**:用户提供文件、表格、badcase、长文档,或者标签/边界/字段含义有歧义时,先用 `data_agent_prepare_generation_goal` 做目标 review;目标确认后再做 plan review。
|
||||||
|
|
||||||
|
所有数据产物必须放在当前用户当前会话的 output 目录下。不要把 records、draft 或 validation 写到项目根目录的 `output/`、`tasks/` 或其他源码目录。工具会把相对 `output_path` 自动路由到会话 output 目录;展示给用户时以工具返回的实际路径为准。
|
||||||
|
|
||||||
开始生成前必须确认这些信息:
|
开始生成前必须确认这些信息:
|
||||||
|
|
||||||
- `dataset_label`:数据集或专题名称。
|
- `dataset_label`:数据集或专题名称。
|
||||||
@@ -61,12 +68,31 @@ source_refs:
|
|||||||
|
|
||||||
如果任一信息缺失,不要生成数据,不要调用 `data_agent_prepare_generation_plan`,不要调用 `data_agent_normalize_dataset_draft`,不要调用 `data_agent_validate_dataset_records`,只向用户提出需要确认的问题。
|
如果任一信息缺失,不要生成数据,不要调用 `data_agent_prepare_generation_plan`,不要调用 `data_agent_normalize_dataset_draft`,不要调用 `data_agent_validate_dataset_records`,只向用户提出需要确认的问题。
|
||||||
|
|
||||||
信息完整后,调用 `data_agent_prepare_generation_goal` 创建 pending goal。这个工具会暂停本轮,必须把返回的 `generation_goal` 展示给用户 review。用户确认 goal 之后,调用 `data_agent_confirm_generation_goal` 获取 `confirmed_goal_id`,再调用 `data_agent_prepare_generation_plan` 创建 pending plan。创建 plan 后也会暂停本轮,必须等待用户 review。
|
两段确认模式下,信息完整后,调用 `data_agent_prepare_generation_goal` 创建 pending goal。这个工具会暂停本轮,必须把返回的 `generation_goal` 展示给用户 review。用户确认 goal 之后,调用 `data_agent_confirm_generation_goal` 获取 `confirmed_goal_id`,再调用 `data_agent_prepare_generation_plan` 创建 pending plan。创建 plan 后也会暂停本轮,必须等待用户 review。
|
||||||
|
|
||||||
|
一次确认模式下,不要先创建 generation goal;直接创建 pending plan,并在 plan 里包含 `target_definitions`、`total_count`、`turn_mix`、`coverage`、`exclusions`、`output_path`。不要让用户先确认目标再确认计划。
|
||||||
|
|
||||||
用户确认后,拿到 `confirmed_plan_id`,才能生成 dataset draft text,并继续调用工具。
|
用户确认后,拿到 `confirmed_plan_id`,才能生成 dataset draft text,并继续调用工具。
|
||||||
|
|
||||||
`data_agent_normalize_dataset_draft` 对生成数据有代码级门禁:没有 `confirmed_plan_id`,或者计划未确认,会拒绝执行。
|
`data_agent_normalize_dataset_draft` 对生成数据有代码级门禁:没有 `confirmed_plan_id`,或者计划未确认,会拒绝执行。
|
||||||
|
|
||||||
|
如果用户在原始需求里已经写出 `Agent(tag="xxx")`、function 调用或其他完整标签表达,`target` 必须原样保留这个完整表达,不要简化成纯标签名。例如用户说 `Agent(tag="餐饮服务")`,则 `target_definitions[*].target` 和后续 draft 的 `target:` 都必须写 `Agent(tag="餐饮服务")`,不要写成 `餐饮服务`。
|
||||||
|
|
||||||
|
review 展示必须简短清晰,不要重复解释工具和流程。每次 review 最多展示 6 行,格式优先如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
我先把生成目标整理好了,先确认边界,暂时不生成数据。
|
||||||
|
- 数据集:xxx
|
||||||
|
- 标签:A -> Agent(tag="A");B -> Agent(tag="B")
|
||||||
|
- 边界:一句话说明核心判定规则
|
||||||
|
- 覆盖:一句话说明主要 case 类型
|
||||||
|
- 内部:goal_id `...`,revision `...`
|
||||||
|
|
||||||
|
你看这个目标是否准确?没问题就回“确认目标”;想改的话直接说哪里不对。
|
||||||
|
```
|
||||||
|
|
||||||
|
计划 review 也最多展示 6 行,只展示数量、轮次、覆盖、输出路径和确认口令。不要把 goal 的完整内容再次复制到 plan review 中,开头必须说明“目标已确认,现在只补充生成参数,标签边界沿用上一步”。确认口令可以自然一点,例如“如果这个数量和路径可以,就回‘确认,开始生成’;想调整就直接说,比如‘改成 20 条,全单轮’。”
|
||||||
|
|
||||||
多标签边界数据不要拆成多个互不相关的单标签计划。应该创建一个计划,并在 `target_definitions` 中列出所有候选标签。例如:
|
多标签边界数据不要拆成多个互不相关的单标签计划。应该创建一个计划,并在 `target_definitions` 中列出所有候选标签。例如:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -96,9 +122,9 @@ source_refs:
|
|||||||
2. 用 `data_agent_render_source_context` 把输入渲染成大模型可读 evidence text。
|
2. 用 `data_agent_render_source_context` 把输入渲染成大模型可读 evidence text。
|
||||||
3. 大模型只基于 evidence text 抽取 `generation_goal`,包括 `dataset_label`、`target_definitions`、`plan_hint`、`coverage`、`exclusions`、`open_questions`、`source_refs`。
|
3. 大模型只基于 evidence text 抽取 `generation_goal`,包括 `dataset_label`、`target_definitions`、`plan_hint`、`coverage`、`exclusions`、`open_questions`、`source_refs`。
|
||||||
4. 如果目标标签、边界或字段含义不清楚,先用普通回复向用户提问并停止。
|
4. 如果目标标签、边界或字段含义不清楚,先用普通回复向用户提问并停止。
|
||||||
5. 信息足够时,调用 `data_agent_prepare_generation_goal` 创建 pending goal,并停止等待用户 review。
|
5. 如果是手写规则且信息完整,调用 `data_agent_prepare_generation_plan`,设置 `direct_review=true`,创建一次确认的 pending 计划,并停止等待用户 review。
|
||||||
6. 用户确认 generation goal 后,调用 `data_agent_confirm_generation_goal` 获取 `confirmed_goal_id`。
|
6. 如果是文件/示例归纳/歧义场景,调用 `data_agent_prepare_generation_goal` 创建 pending goal,并停止等待用户 review。
|
||||||
7. 调用 `data_agent_prepare_generation_plan` 创建 pending 计划,必须传入 `confirmed_goal_id`。
|
7. 用户确认 generation goal 后,调用 `data_agent_confirm_generation_goal` 获取 `confirmed_goal_id`,再调用 `data_agent_prepare_generation_plan` 创建 pending 计划。
|
||||||
8. 展示计划后停止本轮,等待用户 review。
|
8. 展示计划后停止本轮,等待用户 review。
|
||||||
9. 用户提出修改意见时,调用 `data_agent_update_generation_plan`,再展示计划。
|
9. 用户提出修改意见时,调用 `data_agent_update_generation_plan`,再展示计划。
|
||||||
10. 用户明确确认当前计划版本后,调用 `data_agent_confirm_generation_plan`。
|
10. 用户明确确认当前计划版本后,调用 `data_agent_confirm_generation_plan`。
|
||||||
@@ -201,9 +227,9 @@ canonical records 落盘必须使用 `data_agent_export_dataset_records`,默
|
|||||||
1. 直接从用户描述里抽取标签边界。
|
1. 直接从用户描述里抽取标签边界。
|
||||||
2. 多标签边界必须使用 `target_definitions`,不要拆成多个无关单标签计划。
|
2. 多标签边界必须使用 `target_definitions`,不要拆成多个无关单标签计划。
|
||||||
3. 识别规则中的冲突词、优先级和反例。
|
3. 识别规则中的冲突词、优先级和反例。
|
||||||
4. 整理为 `generation_goal` 并调用 `data_agent_prepare_generation_goal`。
|
4. 如果用户已经给出完整 target 表达,直接写入 `target_definitions`;如果 target 表达不明确,先提出问题。
|
||||||
5. 对不明确的 target、数量、单轮/多轮比例、输出路径提出问题。
|
5. 如果数量、轮次或输出路径未指定,可以由模型给出保守建议,走一次确认模式;不要为了这些默认参数单独多问一轮。
|
||||||
6. 用户确认 generation goal 后,调用 `data_agent_confirm_generation_goal`,再调用 `data_agent_prepare_generation_plan`。
|
6. 调用 `data_agent_prepare_generation_plan`,传入 `direct_review=true`,让用户一次确认目标和生成参数。
|
||||||
|
|
||||||
### 示例归纳型
|
### 示例归纳型
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ class AgentPromptingTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
prompt = render_system_prompt(parts)
|
prompt = render_system_prompt(parts)
|
||||||
self.assertIn('# System', prompt)
|
self.assertIn('# 系统规则', prompt)
|
||||||
self.assertIn('# Doing tasks', prompt)
|
self.assertIn('# 处理任务', prompt)
|
||||||
self.assertIn('# Using your tools', prompt)
|
self.assertIn('# 使用工具', prompt)
|
||||||
self.assertIn('# Environment', prompt)
|
self.assertIn('# Skills', prompt)
|
||||||
|
self.assertIn('product-data', prompt)
|
||||||
self.assertIn('__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__', prompt)
|
self.assertIn('__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__', prompt)
|
||||||
self.assertIn('Primary working directory:', prompt)
|
self.assertIn('主工作目录:', prompt)
|
||||||
|
|
||||||
def test_session_state_exports_messages_in_order(self) -> None:
|
def test_session_state_exports_messages_in_order(self) -> None:
|
||||||
state = AgentSessionState.create(['sys one', 'sys two'], 'hello')
|
state = AgentSessionState.create(['sys one', 'sys two'], 'hello')
|
||||||
@@ -57,9 +58,9 @@ class AgentPromptingTests(unittest.TestCase):
|
|||||||
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
|
runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)),
|
||||||
)
|
)
|
||||||
prompt = agent.render_system_prompt()
|
prompt = agent.render_system_prompt()
|
||||||
self.assertIn('Claw Code Python', prompt)
|
self.assertIn('Zhongkong Agent', prompt)
|
||||||
self.assertIn('# System', prompt)
|
self.assertIn('# 系统规则', prompt)
|
||||||
self.assertIn('# Environment', prompt)
|
self.assertIn('# 环境信息', prompt)
|
||||||
|
|
||||||
def test_prompt_builder_mentions_plugins_when_cache_is_loaded(self) -> None:
|
def test_prompt_builder_mentions_plugins_when_cache_is_loaded(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ class AgentRuntimeTests(unittest.TestCase):
|
|||||||
self.assertEqual(child_model.model, 'child-model')
|
self.assertEqual(child_model.model, 'child-model')
|
||||||
self.assertEqual(sorted(child_tools), ['grep_search', 'read_file'])
|
self.assertEqual(sorted(child_tools), ['grep_search', 'read_file'])
|
||||||
|
|
||||||
|
def test_builtin_child_model_alias_inherits_non_claude_parent_model(self) -> None:
|
||||||
|
agent = LocalCodingAgent(
|
||||||
|
model_config=ModelConfig(model='xiaomi/mimo-v2.5-pro'),
|
||||||
|
runtime_config=AgentRuntimeConfig(cwd=Path.cwd()),
|
||||||
|
)
|
||||||
|
agent_def = agent._resolve_agent_definition({'subagent_type': 'Explore'})
|
||||||
|
child_model = agent._resolve_child_model_config({}, agent_def)
|
||||||
|
|
||||||
|
self.assertEqual(agent_def.model, 'haiku')
|
||||||
|
self.assertEqual(child_model.model, 'xiaomi/mimo-v2.5-pro')
|
||||||
|
|
||||||
def test_openai_client_parses_tool_calls(self) -> None:
|
def test_openai_client_parses_tool_calls(self) -> None:
|
||||||
responses = [
|
responses = [
|
||||||
{
|
{
|
||||||
@@ -322,9 +333,9 @@ class AgentRuntimeTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result.stop_reason, 'user_review_required')
|
self.assertEqual(result.stop_reason, 'user_review_required')
|
||||||
self.assertEqual(result.tool_calls, 1)
|
self.assertEqual(result.tool_calls, 1)
|
||||||
self.assertIn('本轮已暂停', result.final_output)
|
self.assertIn('先确认边界', result.final_output)
|
||||||
self.assertIn('goal_id', result.final_output)
|
self.assertIn('goal_id', result.final_output)
|
||||||
self.assertIn('计划提示', result.final_output)
|
self.assertIn('下一步', result.final_output)
|
||||||
self.assertIn('建议先生成 10 条单轮', result.final_output)
|
self.assertIn('建议先生成 10 条单轮', result.final_output)
|
||||||
review_messages = [
|
review_messages = [
|
||||||
entry
|
entry
|
||||||
@@ -333,7 +344,7 @@ class AgentRuntimeTests(unittest.TestCase):
|
|||||||
and entry.get('stop_reason') == 'user_review_required'
|
and entry.get('stop_reason') == 'user_review_required'
|
||||||
]
|
]
|
||||||
self.assertEqual(len(review_messages), 1)
|
self.assertEqual(len(review_messages), 1)
|
||||||
self.assertIn('数据生成目标', str(review_messages[0].get('content', '')))
|
self.assertIn('生成目标', str(review_messages[0].get('content', '')))
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any(event.get('type') == 'user_review_required' for event in result.events)
|
any(event.get('type') == 'user_review_required' for event in result.events)
|
||||||
)
|
)
|
||||||
@@ -416,8 +427,9 @@ class AgentRuntimeTests(unittest.TestCase):
|
|||||||
result = agent.run('Prepare data generation plan')
|
result = agent.run('Prepare data generation plan')
|
||||||
|
|
||||||
self.assertEqual(result.stop_reason, 'user_review_required')
|
self.assertEqual(result.stop_reason, 'user_review_required')
|
||||||
self.assertIn('数据生成计划', result.final_output)
|
self.assertIn('只补充生成参数', result.final_output)
|
||||||
self.assertIn('plan_id', result.final_output)
|
self.assertIn('plan_id', result.final_output)
|
||||||
|
self.assertNotIn('target_definitions', result.final_output)
|
||||||
review_messages = [
|
review_messages = [
|
||||||
entry
|
entry
|
||||||
for entry in result.transcript
|
for entry in result.transcript
|
||||||
@@ -425,7 +437,7 @@ class AgentRuntimeTests(unittest.TestCase):
|
|||||||
and entry.get('stop_reason') == 'user_review_required'
|
and entry.get('stop_reason') == 'user_review_required'
|
||||||
]
|
]
|
||||||
self.assertEqual(len(review_messages), 1)
|
self.assertEqual(len(review_messages), 1)
|
||||||
self.assertIn('数据生成计划', str(review_messages[0].get('content', '')))
|
self.assertIn('生成参数', str(review_messages[0].get('content', '')))
|
||||||
|
|
||||||
def test_write_tool_is_blocked_without_permission(self) -> None:
|
def test_write_tool_is_blocked_without_permission(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
|||||||
@@ -229,6 +229,50 @@ target: Agent(tag="life_service")
|
|||||||
'建议先生成 2 条单轮,输出到 tasks/demo/records.jsonl',
|
'建议先生成 2 条单轮,输出到 tasks/demo/records.jsonl',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_generation_goal_requires_declared_targets_before_review(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
with self.assertRaisesRegex(Exception, 'must include target or target_definitions'):
|
||||||
|
prepare_generation_goal(
|
||||||
|
root=tmp_dir,
|
||||||
|
dataset_label='地图和生活边界数据',
|
||||||
|
goal_summary='生成附近生活服务边界数据',
|
||||||
|
coverage='附近吃喝玩乐',
|
||||||
|
exclusions='不要生成导航路线类 query',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_generation_plan_inherits_targets_from_confirmed_goal(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
goal_payload = prepare_generation_goal(
|
||||||
|
root=tmp_dir,
|
||||||
|
dataset_label='餐饮和导航边界数据',
|
||||||
|
goal_summary='生成餐饮服务和地图导航边界数据',
|
||||||
|
target_definitions=[
|
||||||
|
{'name': '餐饮服务', 'target': 'Agent(tag="餐饮服务")', 'rule': '找附近餐饮'},
|
||||||
|
{'name': '地图导航', 'target': 'Agent(tag="地图导航")', 'rule': '明确导航'},
|
||||||
|
],
|
||||||
|
coverage='餐饮服务和地图导航边界',
|
||||||
|
exclusions='不要生成无关闲聊',
|
||||||
|
)
|
||||||
|
goal_id = goal_payload['goal']['goal_id']
|
||||||
|
confirm_generation_goal(root=tmp_dir, goal_id=goal_id, confirmation='确认目标')
|
||||||
|
|
||||||
|
plan_payload = prepare_generation_plan(
|
||||||
|
root=tmp_dir,
|
||||||
|
confirmed_goal_id=goal_id,
|
||||||
|
dataset_label='餐饮和导航边界数据',
|
||||||
|
target='',
|
||||||
|
total_count=2,
|
||||||
|
turn_mix='2 条单轮',
|
||||||
|
coverage='餐饮服务和地图导航边界',
|
||||||
|
exclusions='不要生成无关闲聊',
|
||||||
|
output_path='tasks/demo/artifacts',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[item['target'] for item in plan_payload['plan']['target_definitions']],
|
||||||
|
['Agent(tag="餐饮服务")', 'Agent(tag="地图导航")'],
|
||||||
|
)
|
||||||
|
|
||||||
def test_generation_plan_dedupes_existing_task_output_path(self) -> None:
|
def test_generation_plan_dedupes_existing_task_output_path(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
existing = Path(tmp_dir) / 'tasks' / 'demo' / 'artifacts'
|
existing = Path(tmp_dir) / 'tasks' / 'demo' / 'artifacts'
|
||||||
@@ -488,6 +532,55 @@ target: Agent(tag="life_service")
|
|||||||
self.assertEqual(export_payload['record_count'], 1)
|
self.assertEqual(export_payload['record_count'], 1)
|
||||||
self.assertEqual(len(exported_lines), 1)
|
self.assertEqual(len(exported_lines), 1)
|
||||||
|
|
||||||
|
def test_data_agent_tools_route_relative_outputs_to_session_output(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
root = Path(tmp_dir)
|
||||||
|
scratchpad = root / '.port_sessions' / 'accounts' / 'alice' / 'sessions' / 's1' / 'scratchpad'
|
||||||
|
scratchpad.mkdir(parents=True)
|
||||||
|
context = build_tool_context(AgentRuntimeConfig(cwd=root), scratchpad_directory=scratchpad)
|
||||||
|
registry = default_tool_registry()
|
||||||
|
|
||||||
|
plan_result = execute_tool(
|
||||||
|
registry,
|
||||||
|
'data_agent_prepare_generation_plan',
|
||||||
|
{
|
||||||
|
'direct_review': True,
|
||||||
|
'dataset_label': '地图和生活边界数据',
|
||||||
|
'target': 'Agent(tag="life_service")',
|
||||||
|
'total_count': 1,
|
||||||
|
'turn_mix': '1 条单轮',
|
||||||
|
'coverage': '附近吃喝玩乐',
|
||||||
|
'exclusions': '不要生成导航路线类 query',
|
||||||
|
'output_path': 'output/demo/records.jsonl',
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
self.assertTrue(plan_result.ok, plan_result.content)
|
||||||
|
plan_payload = json.loads(plan_result.content)
|
||||||
|
|
||||||
|
records = normalize_dataset_draft(
|
||||||
|
'''
|
||||||
|
# dataset_label: 地图和生活边界数据
|
||||||
|
### case: 附近餐饮查询
|
||||||
|
用户: 附近有什么好吃的
|
||||||
|
target: Agent(tag="life_service")
|
||||||
|
'''.strip(),
|
||||||
|
batch_id='demo',
|
||||||
|
base_timestamp=1_755_567_930_500,
|
||||||
|
)['records']
|
||||||
|
export_result = execute_tool(
|
||||||
|
registry,
|
||||||
|
'data_agent_export_dataset_records',
|
||||||
|
{'records': records, 'output_path': 'output/demo/records.jsonl'},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
self.assertTrue(export_result.ok, export_result.content)
|
||||||
|
export_payload = json.loads(export_result.content)
|
||||||
|
|
||||||
|
expected = '.port_sessions/accounts/alice/sessions/s1/output/demo/records.jsonl'
|
||||||
|
self.assertEqual(plan_payload['plan']['output_path'], expected)
|
||||||
|
self.assertEqual(export_payload['output_path'], expected)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user