- 新增三个核心子图:人工审核、意图理解、格式化输出 - 实现完整的审核 API 端点(/api/review/*) - 前端添加审核确认界面(右下角固定框) - 为每个子图创建分步测试代码 - 添加功能实现文档
466 lines
11 KiB
Python
466 lines
11 KiB
Python
"""
|
||
人工审核工具模块
|
||
提供 LangGraph interrupt 机制和状态持久化能力
|
||
|
||
功能:
|
||
1. HumanReview - 人工审核数据类
|
||
2. ReviewStatus - 审核状态枚举
|
||
3. HumanReviewStore - 审核存储接口
|
||
4. InMemoryReviewStore - 内存存储实现
|
||
5. HumanReviewNode - LangGraph 审核节点
|
||
6. ReviewManager - 审核管理器
|
||
"""
|
||
|
||
from typing import Dict, List, Any, Optional, Callable
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum, auto
|
||
from abc import ABC, abstractmethod
|
||
from datetime import datetime
|
||
import uuid
|
||
|
||
|
||
class ReviewStatus(Enum):
|
||
"""审核状态枚举"""
|
||
PENDING = auto() # 待审核
|
||
APPROVED = auto() # 已通过
|
||
REJECTED = auto() # 已拒绝
|
||
MODIFIED = auto() # 已修改
|
||
TIMEOUT = auto() # 已超时
|
||
|
||
|
||
@dataclass
|
||
class HumanReview:
|
||
"""人工审核数据类"""
|
||
review_id: str # 审核ID
|
||
thread_id: str # 线程ID
|
||
user_id: str # 用户ID
|
||
status: ReviewStatus # 审核状态
|
||
content_to_review: str # 待审核内容
|
||
review_comment: str = "" # 审核意见
|
||
modified_content: str = "" # 修改后的内容
|
||
created_at: datetime = field(default_factory=datetime.now)
|
||
reviewed_at: Optional[datetime] = None
|
||
reviewer: Optional[str] = None
|
||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||
|
||
|
||
class HumanReviewStore(ABC):
|
||
"""审核存储接口"""
|
||
|
||
@abstractmethod
|
||
def save(self, review: HumanReview) -> None:
|
||
"""
|
||
保存审核
|
||
|
||
Args:
|
||
review: 审核对象
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get(self, review_id: str) -> Optional[HumanReview]:
|
||
"""
|
||
获取审核
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
|
||
Returns:
|
||
审核对象,如果不存在返回 None
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_by_thread(self, thread_id: str) -> List[HumanReview]:
|
||
"""
|
||
获取线程的所有审核
|
||
|
||
Args:
|
||
thread_id: 线程ID
|
||
|
||
Returns:
|
||
审核列表
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_pending(self, limit: int = 100) -> List[HumanReview]:
|
||
"""
|
||
获取待审核的列表
|
||
|
||
Args:
|
||
limit: 返回数量限制
|
||
|
||
Returns:
|
||
待审核列表
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def update_status(
|
||
self,
|
||
review_id: str,
|
||
status: ReviewStatus,
|
||
reviewer: Optional[str] = None,
|
||
comment: str = "",
|
||
modified_content: str = ""
|
||
) -> bool:
|
||
"""
|
||
更新审核状态
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
status: 新状态
|
||
reviewer: 审核人
|
||
comment: 审核意见
|
||
modified_content: 修改后的内容
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
pass
|
||
|
||
|
||
class InMemoryReviewStore(HumanReviewStore):
|
||
"""内存存储实现"""
|
||
|
||
def __init__(self):
|
||
self._reviews: Dict[str, HumanReview] = {}
|
||
|
||
def save(self, review: HumanReview) -> None:
|
||
"""
|
||
保存审核
|
||
|
||
Args:
|
||
review: 审核对象
|
||
"""
|
||
self._reviews[review.review_id] = review
|
||
|
||
def get(self, review_id: str) -> Optional[HumanReview]:
|
||
"""
|
||
获取审核
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
|
||
Returns:
|
||
审核对象,如果不存在返回 None
|
||
"""
|
||
return self._reviews.get(review_id)
|
||
|
||
def get_by_thread(self, thread_id: str) -> List[HumanReview]:
|
||
"""
|
||
获取线程的所有审核
|
||
|
||
Args:
|
||
thread_id: 线程ID
|
||
|
||
Returns:
|
||
审核列表
|
||
"""
|
||
return [
|
||
review for review in self._reviews.values()
|
||
if review.thread_id == thread_id
|
||
]
|
||
|
||
def get_pending(self, limit: int = 100) -> List[HumanReview]:
|
||
"""
|
||
获取待审核的列表
|
||
|
||
Args:
|
||
limit: 返回数量限制
|
||
|
||
Returns:
|
||
待审核列表
|
||
"""
|
||
pending = [
|
||
review for review in self._reviews.values()
|
||
if review.status == ReviewStatus.PENDING
|
||
]
|
||
pending.sort(key=lambda r: r.created_at)
|
||
return pending[:limit]
|
||
|
||
def update_status(
|
||
self,
|
||
review_id: str,
|
||
status: ReviewStatus,
|
||
reviewer: Optional[str] = None,
|
||
comment: str = "",
|
||
modified_content: str = ""
|
||
) -> bool:
|
||
"""
|
||
更新审核状态
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
status: 新状态
|
||
reviewer: 审核人
|
||
comment: 审核意见
|
||
modified_content: 修改后的内容
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
review = self._reviews.get(review_id)
|
||
if review is None:
|
||
return False
|
||
|
||
review.status = status
|
||
review.review_comment = comment
|
||
review.modified_content = modified_content
|
||
review.reviewer = reviewer
|
||
review.reviewed_at = datetime.now()
|
||
return True
|
||
|
||
|
||
class HumanReviewNode:
|
||
"""LangGraph 审核节点"""
|
||
|
||
def __init__(
|
||
self,
|
||
store: HumanReviewStore,
|
||
should_review: Optional[Callable[[Any], bool]] = None
|
||
):
|
||
"""
|
||
初始化审核节点
|
||
|
||
Args:
|
||
store: 审核存储
|
||
should_review: 判断是否需要审核的函数
|
||
"""
|
||
self.store = store
|
||
self.should_review = should_review or (lambda state: True)
|
||
|
||
def create_review(
|
||
self,
|
||
state: Any,
|
||
thread_id: str,
|
||
user_id: str,
|
||
content_to_review: str
|
||
) -> str:
|
||
"""
|
||
创建审核
|
||
|
||
Args:
|
||
state: 状态
|
||
thread_id: 线程ID
|
||
user_id: 用户ID
|
||
content_to_review: 待审核内容
|
||
|
||
Returns:
|
||
审核ID
|
||
"""
|
||
review_id = str(uuid.uuid4())
|
||
review = HumanReview(
|
||
review_id=review_id,
|
||
thread_id=thread_id,
|
||
user_id=user_id,
|
||
status=ReviewStatus.PENDING,
|
||
content_to_review=content_to_review
|
||
)
|
||
self.store.save(review)
|
||
return review_id
|
||
|
||
def check_review_status(self, review_id: str) -> Optional[ReviewStatus]:
|
||
"""
|
||
检查审核状态
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
|
||
Returns:
|
||
审核状态,如果不存在返回 None
|
||
"""
|
||
review = self.store.get(review_id)
|
||
return review.status if review else None
|
||
|
||
def get_review_result(self, review_id: str) -> Optional[HumanReview]:
|
||
"""
|
||
获取审核结果
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
|
||
Returns:
|
||
审核对象,如果不存在返回 None
|
||
"""
|
||
return self.store.get(review_id)
|
||
|
||
async def __call__(self, state: Any) -> Dict[str, Any]:
|
||
"""
|
||
节点执行方法(LangGraph 兼容)
|
||
|
||
Args:
|
||
state: 状态
|
||
|
||
Returns:
|
||
更新后的状态
|
||
"""
|
||
# 检查是否需要审核
|
||
if not self.should_review(state):
|
||
return {"review_skipped": True}
|
||
|
||
# 从状态中提取信息
|
||
thread_id = getattr(state, "thread_id", str(uuid.uuid4()))
|
||
user_id = getattr(state, "user_id", "default_user")
|
||
|
||
# 获取待审核内容
|
||
content_to_review = ""
|
||
if hasattr(state, "messages") and state.messages:
|
||
last_msg = state.messages[-1] if state.messages else None
|
||
if last_msg and hasattr(last_msg, "content"):
|
||
content_to_review = last_msg.content
|
||
|
||
# 创建审核
|
||
review_id = self.create_review(state, thread_id, user_id, content_to_review)
|
||
|
||
# 返回状态更新
|
||
return {
|
||
"review_id": review_id,
|
||
"review_pending": True,
|
||
"interrupt": True # 标记需要中断
|
||
}
|
||
|
||
|
||
class ReviewManager:
|
||
"""审核管理器"""
|
||
|
||
def __init__(self, store: Optional[HumanReviewStore] = None):
|
||
"""
|
||
初始化审核管理器
|
||
|
||
Args:
|
||
store: 审核存储
|
||
"""
|
||
self.store = store or InMemoryReviewStore()
|
||
|
||
def request_review(
|
||
self,
|
||
thread_id: str,
|
||
user_id: str,
|
||
content: str,
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
) -> str:
|
||
"""
|
||
请求审核
|
||
|
||
Args:
|
||
thread_id: 线程ID
|
||
user_id: 用户ID
|
||
content: 待审核内容
|
||
metadata: 元数据
|
||
|
||
Returns:
|
||
审核ID
|
||
"""
|
||
review_id = str(uuid.uuid4())
|
||
review = HumanReview(
|
||
review_id=review_id,
|
||
thread_id=thread_id,
|
||
user_id=user_id,
|
||
status=ReviewStatus.PENDING,
|
||
content_to_review=content,
|
||
metadata=metadata or {}
|
||
)
|
||
self.store.save(review)
|
||
return review_id
|
||
|
||
def approve(
|
||
self,
|
||
review_id: str,
|
||
reviewer: str,
|
||
comment: str = ""
|
||
) -> bool:
|
||
"""
|
||
审核通过
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
reviewer: 审核人
|
||
comment: 审核意见
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
return self.store.update_status(
|
||
review_id=review_id,
|
||
status=ReviewStatus.APPROVED,
|
||
reviewer=reviewer,
|
||
comment=comment
|
||
)
|
||
|
||
def reject(
|
||
self,
|
||
review_id: str,
|
||
reviewer: str,
|
||
comment: str = ""
|
||
) -> bool:
|
||
"""
|
||
审核拒绝
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
reviewer: 审核人
|
||
comment: 审核意见
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
return self.store.update_status(
|
||
review_id=review_id,
|
||
status=ReviewStatus.REJECTED,
|
||
reviewer=reviewer,
|
||
comment=comment
|
||
)
|
||
|
||
def modify(
|
||
self,
|
||
review_id: str,
|
||
reviewer: str,
|
||
modified_content: str,
|
||
comment: str = ""
|
||
) -> bool:
|
||
"""
|
||
审核修改
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
reviewer: 审核人
|
||
modified_content: 修改后的内容
|
||
comment: 审核意见
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
return self.store.update_status(
|
||
review_id=review_id,
|
||
status=ReviewStatus.MODIFIED,
|
||
reviewer=reviewer,
|
||
comment=comment,
|
||
modified_content=modified_content
|
||
)
|
||
|
||
def get_pending_reviews(self, limit: int = 100) -> List[HumanReview]:
|
||
"""
|
||
获取待审核列表
|
||
|
||
Args:
|
||
limit: 返回数量限制
|
||
|
||
Returns:
|
||
待审核列表
|
||
"""
|
||
return self.store.get_pending(limit)
|
||
|
||
def get_review(self, review_id: str) -> Optional[HumanReview]:
|
||
"""
|
||
获取审核详情
|
||
|
||
Args:
|
||
review_id: 审核ID
|
||
|
||
Returns:
|
||
审核对象,如果不存在返回 None
|
||
"""
|
||
return self.store.get(review_id)
|