refactor: 移除过时的审批逻辑和相关代码,优化选项处理
This commit is contained in:
parent
e9bf749307
commit
9a0541eb7d
@ -1,6 +1,3 @@
|
|||||||
# debug 工具包
|
# debug 工具包
|
||||||
from .tools import get_approved_user_goal
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = []
|
||||||
"get_approved_user_goal",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -256,14 +256,6 @@ def _build_ask_user_question_payload(info: Any, thread_id: str) -> dict[str, Any
|
|||||||
operation = payload.get("operation")
|
operation = payload.get("operation")
|
||||||
|
|
||||||
options = _normalize_interrupt_options(payload.get("options"))
|
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 {
|
return {
|
||||||
"question_id": question_id,
|
"question_id": question_id,
|
||||||
|
|||||||
161
test/test_chat_stream_interrupt.py
Normal file
161
test/test_chat_stream_interrupt.py
Normal 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"])
|
||||||
@ -18,27 +18,17 @@ const normalizeOptions = (rawOptions) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toLegacyApprovalOptions = () => [
|
|
||||||
{ label: '批准 (Recommended)', value: 'approve' },
|
|
||||||
{ label: '拒绝', value: 'reject' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const extractQuestionPayload = (chunk) => {
|
const extractQuestionPayload = (chunk) => {
|
||||||
const interruptInfo = chunk?.interrupt_info || {}
|
const interruptInfo = chunk?.interrupt_info || {}
|
||||||
const rawOptions =
|
const rawOptions = chunk?.options || interruptInfo?.options || []
|
||||||
chunk?.options || interruptInfo?.options || (interruptInfo?.operation ? toLegacyApprovalOptions() : [])
|
|
||||||
const options = normalizeOptions(rawOptions)
|
const options = normalizeOptions(rawOptions)
|
||||||
const operation = chunk?.operation || interruptInfo?.operation || ''
|
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)
|
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 questionId = chunk?.question_id || interruptInfo?.question_id || ''
|
||||||
const question = chunk?.question || interruptInfo?.question || '请选择一个选项'
|
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 {
|
return {
|
||||||
questionId,
|
questionId,
|
||||||
@ -47,17 +37,10 @@ const extractQuestionPayload = (chunk) => {
|
|||||||
multiSelect,
|
multiSelect,
|
||||||
allowOther,
|
allowOther,
|
||||||
source,
|
source,
|
||||||
operation,
|
operation
|
||||||
legacyMode
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inferApprovedFromAnswer = (answer) => {
|
|
||||||
if (answer === 'approve') return true
|
|
||||||
if (answer === 'reject') return false
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessages }) {
|
export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessages }) {
|
||||||
const approvalState = reactive({
|
const approvalState = reactive({
|
||||||
showModal: false,
|
showModal: false,
|
||||||
@ -68,7 +51,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allowOther: true,
|
allowOther: true,
|
||||||
source: '',
|
source: '',
|
||||||
legacyMode: false,
|
|
||||||
threadId: null
|
threadId: null
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -98,15 +80,11 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
resetOnGoingConv(threadId)
|
resetOnGoingConv(threadId)
|
||||||
threadState.streamAbortController = new AbortController()
|
threadState.streamAbortController = new AbortController()
|
||||||
|
|
||||||
const approved = inferApprovedFromAnswer(answer)
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
thread_id: threadId,
|
thread_id: threadId,
|
||||||
answer,
|
answer,
|
||||||
config: agentConfigId ? { agent_config_id: agentConfigId } : {}
|
config: agentConfigId ? { agent_config_id: agentConfigId } : {}
|
||||||
}
|
}
|
||||||
if (approved !== null) {
|
|
||||||
requestBody.approved = approved
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await agentApi.resumeAgentChat(currentAgentId, requestBody, {
|
const response = await agentApi.resumeAgentChat(currentAgentId, requestBody, {
|
||||||
@ -139,11 +117,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
if (!threadState) return false
|
if (!threadState) return false
|
||||||
|
|
||||||
const payload = extractQuestionPayload(chunk)
|
const payload = extractQuestionPayload(chunk)
|
||||||
if (!payload.options.length) {
|
|
||||||
payload.options = toLegacyApprovalOptions()
|
|
||||||
payload.legacyMode = true
|
|
||||||
payload.allowOther = false
|
|
||||||
}
|
|
||||||
|
|
||||||
threadState.isStreaming = false
|
threadState.isStreaming = false
|
||||||
|
|
||||||
@ -155,7 +128,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
approvalState.multiSelect = payload.multiSelect
|
approvalState.multiSelect = payload.multiSelect
|
||||||
approvalState.allowOther = payload.allowOther
|
approvalState.allowOther = payload.allowOther
|
||||||
approvalState.source = payload.source
|
approvalState.source = payload.source
|
||||||
approvalState.legacyMode = payload.legacyMode
|
|
||||||
approvalState.threadId = chunk.thread_id || threadId
|
approvalState.threadId = chunk.thread_id || threadId
|
||||||
|
|
||||||
fetchThreadMessages({ agentId: currentAgentId, threadId })
|
fetchThreadMessages({ agentId: currentAgentId, threadId })
|
||||||
@ -172,7 +144,6 @@ export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessa
|
|||||||
approvalState.multiSelect = false
|
approvalState.multiSelect = false
|
||||||
approvalState.allowOther = true
|
approvalState.allowOther = true
|
||||||
approvalState.source = ''
|
approvalState.source = ''
|
||||||
approvalState.legacyMode = false
|
|
||||||
approvalState.threadId = null
|
approvalState.threadId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user