Files
ailine/backend/app/agent_subgraphs/common/human_review.py
root bc26b81f08 feat: 实现完整的人工审核功能与子图模块
- 新增三个核心子图:人工审核、意图理解、格式化输出
- 实现完整的审核 API 端点(/api/review/*)
- 前端添加审核确认界面(右下角固定框)
- 为每个子图创建分步测试代码
- 添加功能实现文档
2026-04-25 13:24:50 +08:00

466 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
人工审核工具模块
提供 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)