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)
|