refactor: 移除过时的审批逻辑和相关代码,优化选项处理

This commit is contained in:
Wenjie Zhang 2026-03-10 00:16:41 +08:00
parent e9bf749307
commit 9a0541eb7d
5 changed files with 166 additions and 104 deletions

View File

@ -1,6 +1,3 @@
# debug 工具包
from .tools import get_approved_user_goal
__all__ = [
"get_approved_user_goal",
]
__all__ = []

View File

@ -1,59 +0,0 @@
from langgraph.types import interrupt
from src.agents.common.toolkits.registry import tool
@tool(category="debug", tags=["内置", "审批"], display_name="人工审批")
def get_approved_user_goal(
operation_description: str,
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
Args:
operation_description: 需要审批的操作描述例如 "调用知识库工具"
Returns:
dict: 包含审批结果的字典格式为 {"approved": bool, "message": str}
"""
# 构建详细的中断信息
interrupt_info = {
"question": "是否批准以下操作?",
"operation": operation_description,
}
# 触发人工审批
interrupt_result = interrupt(interrupt_info)
if isinstance(interrupt_result, bool):
is_approved = interrupt_result
elif isinstance(interrupt_result, str):
is_approved = interrupt_result.strip().lower() in {"approve", "approved", "true", "yes", "1"}
elif isinstance(interrupt_result, list):
lowered = {str(item).strip().lower() for item in interrupt_result}
is_approved = "approve" in lowered or "approved" in lowered
elif isinstance(interrupt_result, dict):
selected = interrupt_result.get("selected")
if isinstance(selected, list):
lowered = {str(item).strip().lower() for item in selected}
is_approved = "approve" in lowered or "approved" in lowered
else:
text = str(interrupt_result.get("text") or "").strip().lower()
is_approved = text in {"approve", "approved", "true", "yes", "1"}
else:
is_approved = bool(interrupt_result)
# 返回审批结果
if is_approved:
result = {
"approved": True,
"message": f"✅ 操作已批准:{operation_description}",
}
print(f"✅ 人工审批通过: {operation_description}")
else:
result = {
"approved": False,
"message": f"❌ 操作被拒绝:{operation_description}",
}
print(f"❌ 人工审批被拒绝: {operation_description}")
return result

View File

@ -256,14 +256,6 @@ def _build_ask_user_question_payload(info: Any, thread_id: str) -> dict[str, Any
operation = payload.get("operation")
options = _normalize_interrupt_options(payload.get("options"))
if not options and isinstance(operation, str) and operation.strip():
# 兼容旧版 get_approved_user_goal 的 interrupt 结构
options = [
{"label": "批准 (Recommended)", "value": "approve"},
{"label": "拒绝", "value": "reject"},
]
source = "get_approved_user_goal"
allow_other = False
return {
"question_id": question_id,

View File

@ -0,0 +1,161 @@
"""测试 chat_stream_service 中的 interrupt 相关函数"""
import pytest
import sys
import os
sys.path.insert(0, os.getcwd())
from src.services.chat_stream_service import (
_normalize_interrupt_options,
_build_ask_user_question_payload,
_coerce_interrupt_payload,
)
class TestNormalizeInterruptOptions:
"""测试 _normalize_interrupt_options 函数"""
def test_empty_input(self):
assert _normalize_interrupt_options(None) == []
assert _normalize_interrupt_options([]) == []
def test_dict_options(self):
raw = [
{"label": "选项1", "value": "option1"},
{"label": "选项2", "value": "option2"},
]
result = _normalize_interrupt_options(raw)
assert len(result) == 2
assert result[0] == {"label": "选项1", "value": "option1"}
assert result[1] == {"label": "选项2", "value": "option2"}
def test_string_options(self):
raw = ["选项1", "选项2", "选项3"]
result = _normalize_interrupt_options(raw)
assert len(result) == 3
assert result[0] == {"label": "选项1", "value": "选项1"}
def test_mixed_options(self):
raw = [{"label": "选项1", "value": "option1"}, "选项2"]
result = _normalize_interrupt_options(raw)
assert len(result) == 2
assert result[0] == {"label": "选项1", "value": "option1"}
assert result[1] == {"label": "选项2", "value": "选项2"}
def test_invalid_options(self):
raw = [{"label": "只有label"}, {}, " "]
result = _normalize_interrupt_options(raw)
assert len(result) == 1 # 只有有效的选项
assert result[0] == {"label": "只有label", "value": "只有label"}
def test_value_only(self):
raw = [{"value": "only_value"}]
result = _normalize_interrupt_options(raw)
assert len(result) == 1
assert result[0] == {"label": "only_value", "value": "only_value"}
class TestBuildAskUserQuestionPayload:
"""测试 _build_ask_user_question_payload 函数"""
def test_basic_question(self):
info = {
"question": "请确认是否继续?",
"options": [
{"label": "确认", "value": "yes"},
{"label": "取消", "value": "no"},
],
}
result = _build_ask_user_question_payload(info, "thread-123")
assert result["question"] == "请确认是否继续?"
assert len(result["options"]) == 2
assert result["options"][0] == {"label": "确认", "value": "yes"}
assert result["options"][1] == {"label": "取消", "value": "no"}
assert result["source"] == "interrupt"
assert result["thread_id"] == "thread-123"
assert result["multi_select"] is False
assert result["allow_other"] is True
def test_question_with_source(self):
info = {
"question": "选择一个选项",
"options": ["A", "B", "C"],
"source": "ask_user_question",
}
result = _build_ask_user_question_payload(info, "thread-456")
assert result["source"] == "ask_user_question"
assert len(result["options"]) == 3
def test_multi_select(self):
info = {
"question": "选择多个",
"options": ["A", "B", "C"],
"multi_select": True,
}
result = _build_ask_user_question_payload(info, "thread-789")
assert result["multi_select"] is True
def test_disable_allow_other(self):
info = {
"question": "只能选择",
"options": ["A", "B"],
"allow_other": False,
}
result = _build_ask_user_question_payload(info, "thread-000")
assert result["allow_other"] is False
def test_with_operation(self):
info = {
"question": "是否执行操作?",
"operation": "删除文件",
"options": [{"label": "批准", "value": "approve"}, {"label": "拒绝", "value": "reject"}],
}
result = _build_ask_user_question_payload(info, "thread-op")
assert result["operation"] == "删除文件"
def test_no_options(self):
"""测试没有 options 的情况 - 不再自动填充 legacy 选项"""
info = {
"question": "请确认?",
}
result = _build_ask_user_question_payload(info, "thread-no-opt")
# 不再有默认的 approve/reject 选项
assert result["options"] == []
assert result["source"] == "interrupt"
def test_question_id_generation(self):
"""测试 question_id 自动生成"""
info = {"question": "测试?"}
result = _build_ask_user_question_payload(info, "thread-id")
# 应该生成了 UUID
assert result["question_id"] != ""
assert len(result["question_id"]) > 0
class TestCoerceInterruptPayload:
"""测试 _coerce_interrupt_payload 函数"""
def test_dict_input(self):
info = {"question": "test?", "options": ["a", "b"]}
result = _coerce_interrupt_payload(info)
assert result == info
def test_string_input(self):
info = "just a string"
result = _coerce_interrupt_payload(info)
assert isinstance(result, dict)
def test_none_input(self):
result = _coerce_interrupt_payload(None)
assert isinstance(result, dict)
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@ -18,27 +18,17 @@ const normalizeOptions = (rawOptions) => {
.filter(Boolean)
}
const toLegacyApprovalOptions = () => [
{ label: '批准 (Recommended)', value: 'approve' },
{ label: '拒绝', value: 'reject' }
]
const extractQuestionPayload = (chunk) => {
const interruptInfo = chunk?.interrupt_info || {}
const rawOptions =
chunk?.options || interruptInfo?.options || (interruptInfo?.operation ? toLegacyApprovalOptions() : [])
const rawOptions = chunk?.options || interruptInfo?.options || []
const options = normalizeOptions(rawOptions)
const operation = chunk?.operation || interruptInfo?.operation || ''
const source = chunk?.source || interruptInfo?.source || (operation ? 'get_approved_user_goal' : 'interrupt')
const source = chunk?.source || interruptInfo?.source || 'interrupt'
const multiSelect = Boolean(chunk?.multi_select ?? interruptInfo?.multi_select ?? false)
let allowOther = Boolean(chunk?.allow_other ?? interruptInfo?.allow_other ?? true)
const allowOther = Boolean(chunk?.allow_other ?? interruptInfo?.allow_other ?? true)
const questionId = chunk?.question_id || interruptInfo?.question_id || ''
const question = chunk?.question || interruptInfo?.question || '请选择一个选项'
const legacyMode = source === 'get_approved_user_goal' && options.length === 2
if (source === 'get_approved_user_goal') {
allowOther = false
}
return {
questionId,
@ -47,17 +37,10 @@ const extractQuestionPayload = (chunk) => {
multiSelect,
allowOther,
source,
operation,
legacyMode
operation
}
}
const inferApprovedFromAnswer = (answer) => {
if (answer === 'approve') return true
if (answer === 'reject') return false
return null
}
export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessages }) {
const approvalState = reactive({
showModal: false,
@ -68,7 +51,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
multiSelect: false,
allowOther: true,
source: '',
legacyMode: false,
threadId: null
})
@ -98,15 +80,11 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
resetOnGoingConv(threadId)
threadState.streamAbortController = new AbortController()
const approved = inferApprovedFromAnswer(answer)
const requestBody = {
thread_id: threadId,
answer,
config: agentConfigId ? { agent_config_id: agentConfigId } : {}
}
if (approved !== null) {
requestBody.approved = approved
}
try {
const response = await agentApi.resumeAgentChat(currentAgentId, requestBody, {
@ -139,11 +117,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
if (!threadState) return false
const payload = extractQuestionPayload(chunk)
if (!payload.options.length) {
payload.options = toLegacyApprovalOptions()
payload.legacyMode = true
payload.allowOther = false
}
threadState.isStreaming = false
@ -155,7 +128,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
approvalState.multiSelect = payload.multiSelect
approvalState.allowOther = payload.allowOther
approvalState.source = payload.source
approvalState.legacyMode = payload.legacyMode
approvalState.threadId = chunk.thread_id || threadId
fetchThreadMessages({ agentId: currentAgentId, threadId })
@ -172,7 +144,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
approvalState.multiSelect = false
approvalState.allowOther = true
approvalState.source = ''
approvalState.legacyMode = false
approvalState.threadId = null
}