feat: 完善人工关注与内容合规配置
This commit is contained in:
174
ai_knowledge_base_v2/apps/backend/app/api/admin_attention.py
Normal file
174
ai_knowledge_base_v2/apps/backend/app/api/admin_attention.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.pagination import page_result
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
from app.models.knowledge import KnowledgeRetrievalLog
|
||||
from app.models.user import User
|
||||
from app.schemas.knowledge import AttentionConfigRequest, AttentionPreviewRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.human_attention_service import HumanAttentionService
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/attention/config")
|
||||
def get_attention_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
return api_success(HumanAttentionService.config_dict(HumanAttentionService.get_config(db)))
|
||||
|
||||
|
||||
@router.put("/attention/config")
|
||||
def save_attention_config(
|
||||
payload: AttentionConfigRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
config = HumanAttentionService.save_config(db, payload.model_dump(), current_admin.id)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="attention", action="config_update")
|
||||
db.commit()
|
||||
return api_success(HumanAttentionService.config_dict(config))
|
||||
|
||||
|
||||
@router.get("/attention/preview/messages")
|
||||
def attention_preview_messages(
|
||||
keyword: str = Query(default="", max_length=100),
|
||||
page: int = Query(default=1, ge=1),
|
||||
pageSize: int = Query(default=10, ge=5, le=50),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
query = (
|
||||
select(ChatMessage, ChatSession, User)
|
||||
.join(ChatSession, ChatSession.id == ChatMessage.session_id)
|
||||
.join(User, User.id == ChatMessage.user_id)
|
||||
.where(ChatMessage.role == "user", ChatMessage.message_status == "FINISHED")
|
||||
)
|
||||
normalized_keyword = keyword.strip()
|
||||
if normalized_keyword:
|
||||
pattern = f"%{normalized_keyword}%"
|
||||
query = query.where(or_(ChatMessage.content.like(pattern), User.name.like(pattern), User.phone.like(pattern)))
|
||||
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||
rows = db.execute(
|
||||
query.order_by(ChatMessage.id.desc()).offset((page - 1) * pageSize).limit(pageSize)
|
||||
).all()
|
||||
items = [_preview_message_dict(db, message, session, user) for message, session, user in rows]
|
||||
return api_success(page_result(items, total=total, page=page, page_size=pageSize))
|
||||
|
||||
|
||||
@router.get("/attention/preview/config")
|
||||
def get_attention_preview_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
return api_success(HumanAttentionService.config_dict(HumanAttentionService.get_config(db)))
|
||||
|
||||
|
||||
@router.post("/attention/preview")
|
||||
def preview_attention_filter(
|
||||
payload: AttentionPreviewRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
messages = db.scalars(
|
||||
select(ChatMessage).where(
|
||||
ChatMessage.id.in_(payload.messageIds),
|
||||
ChatMessage.role == "user",
|
||||
ChatMessage.message_status == "FINISHED",
|
||||
)
|
||||
).all()
|
||||
by_id = {message.id: message for message in messages}
|
||||
missing_ids = [message_id for message_id in payload.messageIds if message_id not in by_id]
|
||||
if missing_ids:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"历史消息不存在:{missing_ids[0]}")
|
||||
results = []
|
||||
for message_id in payload.messageIds:
|
||||
message = by_id[message_id]
|
||||
answer, knowledge_missing = _answer_and_knowledge_state(db, message)
|
||||
decision = HumanAttentionService.preview(
|
||||
db,
|
||||
question=message.content,
|
||||
answer=answer,
|
||||
knowledge_missing=knowledge_missing,
|
||||
config_payload=payload.config.model_dump(),
|
||||
user_id=message.user_id,
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"messageId": message.id,
|
||||
"sessionId": message.session_id,
|
||||
"question": message.content,
|
||||
"answer": answer,
|
||||
"knowledgeMissing": knowledge_missing,
|
||||
"needsAttention": decision.needs_attention,
|
||||
"priority": decision.priority,
|
||||
"reason": decision.reason,
|
||||
"summary": decision.summary,
|
||||
"source": decision.source,
|
||||
"rawOutput": decision.raw_output,
|
||||
"renderedPrompt": decision.rendered_prompt,
|
||||
"matchedItem": decision.matched_item,
|
||||
}
|
||||
)
|
||||
OperationLogService.write(
|
||||
db,
|
||||
admin_id=current_admin.id,
|
||||
module="attention",
|
||||
action=f"preview:{len(results)}",
|
||||
)
|
||||
db.commit()
|
||||
return api_success(results)
|
||||
|
||||
|
||||
def _preview_message_dict(db: Session, message: ChatMessage, session: ChatSession, user: User) -> dict:
|
||||
answer, knowledge_missing = _answer_and_knowledge_state(db, message)
|
||||
return {
|
||||
"messageId": message.id,
|
||||
"sessionId": message.session_id,
|
||||
"userId": message.user_id,
|
||||
"userName": user.name,
|
||||
"userPhone": user.phone,
|
||||
"sessionTitle": session.title,
|
||||
"question": message.content,
|
||||
"answer": answer,
|
||||
"knowledgeMissing": knowledge_missing,
|
||||
"createdAt": message.created_at,
|
||||
}
|
||||
|
||||
|
||||
def _answer_and_knowledge_state(db: Session, user_message: ChatMessage) -> tuple[str, bool]:
|
||||
answer = db.scalar(
|
||||
select(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == user_message.session_id,
|
||||
ChatMessage.user_id == user_message.user_id,
|
||||
ChatMessage.role == "assistant",
|
||||
ChatMessage.id > user_message.id,
|
||||
)
|
||||
.order_by(ChatMessage.id.asc())
|
||||
.limit(1)
|
||||
)
|
||||
if answer is None:
|
||||
return "", False
|
||||
retrieval_log = db.scalar(
|
||||
select(KnowledgeRetrievalLog)
|
||||
.where(KnowledgeRetrievalLog.message_id == answer.id)
|
||||
.order_by(KnowledgeRetrievalLog.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
knowledge_missing = bool(
|
||||
retrieval_log
|
||||
and retrieval_log.knowledge_called == 1
|
||||
and not (retrieval_log.final_section_ids or "").strip()
|
||||
)
|
||||
return answer.content, knowledge_missing
|
||||
@@ -30,6 +30,7 @@ from app.services.agent_debug_service import AgentDebugService
|
||||
from app.services.feishu_service import FeishuKnowledgeService
|
||||
from app.services.knowledge_service import KnowledgeScope
|
||||
from app.services.model_service import ModelClientService
|
||||
from app.services.public_site_config_service import PublicSiteConfigService
|
||||
from app.services.admin_permission_service import require_permission
|
||||
from app.services.reasoning_policy_service import ReasoningPolicyService
|
||||
from app.services.response_style_service import ResponseStyleService
|
||||
@@ -477,6 +478,7 @@ def save_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
payload.configValue = PublicSiteConfigService.normalize_admin_value(payload.configKey, payload.configValue)
|
||||
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == payload.configKey))
|
||||
if config is None:
|
||||
config = SystemConfig(config_key=payload.configKey, config_value=payload.configValue)
|
||||
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.ai_content_label import ensure_ai_generated_notice
|
||||
from app.api.pagination import page_result
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin, get_current_user
|
||||
@@ -146,7 +147,7 @@ def _feedback_workbook(rows: list[tuple]) -> Workbook:
|
||||
sheet = workbook.active
|
||||
sheet.title = "反馈记录"
|
||||
sheet.sheet_view.showGridLines = False
|
||||
headers = ["序号", "状态", "用户姓名", "手机号", "反馈内容", "会话标题", "消息ID", "对应AI回答", "提交时间", "阅读时间"]
|
||||
headers = ["序号", "状态", "用户姓名", "手机号", "反馈内容", "会话标题", "消息ID", "对应AI回答(AI生成)", "提交时间", "阅读时间"]
|
||||
sheet.append(headers)
|
||||
for index, (feedback, user, message, session) in enumerate(rows, start=1):
|
||||
sheet.append([
|
||||
@@ -157,7 +158,7 @@ def _feedback_workbook(rows: list[tuple]) -> Workbook:
|
||||
_excel_safe_text(feedback.content),
|
||||
_excel_safe_text(session.title),
|
||||
message.id,
|
||||
_excel_safe_text(message.content),
|
||||
_excel_safe_text(ensure_ai_generated_notice(message.content)),
|
||||
feedback.created_at,
|
||||
feedback.read_at,
|
||||
])
|
||||
|
||||
16
ai_knowledge_base_v2/apps/backend/app/api/public_config.py
Normal file
16
ai_knowledge_base_v2/apps/backend/app/api/public_config.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.responses import api_success
|
||||
from app.services.public_site_config_service import PublicSiteConfigService
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def public_site_config(db: Session = Depends(get_db)) -> dict:
|
||||
return api_success(PublicSiteConfigService.public_config(db))
|
||||
@@ -5,6 +5,7 @@ from app.core.dependencies import enforce_admin_access
|
||||
|
||||
from app.api import (
|
||||
admin_auth,
|
||||
admin_attention,
|
||||
admin_content_generation,
|
||||
admin_user_behavior,
|
||||
admin_agent_records,
|
||||
@@ -23,6 +24,7 @@ from app.api import (
|
||||
chat,
|
||||
health,
|
||||
integration_sso,
|
||||
public_config,
|
||||
user,
|
||||
voice,
|
||||
behavior,
|
||||
@@ -30,6 +32,7 @@ from app.api import (
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(public_config.router, prefix="/site", tags=["public-site-config"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(integration_sso.router, prefix="/integration/sso", tags=["integration-sso"])
|
||||
api_router.include_router(user.router, prefix="/user", tags=["user"])
|
||||
@@ -40,6 +43,7 @@ api_router.include_router(behavior.router, prefix="/behavior", tags=["user-behav
|
||||
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_management.router, prefix="/admin", tags=["admin-management"])
|
||||
guard = [Depends(enforce_admin_access)]
|
||||
api_router.include_router(admin_attention.router, prefix="/admin", tags=["admin-attention"], dependencies=guard)
|
||||
api_router.include_router(admin_content_generation.router, prefix="/admin", tags=["admin-content-generation"], dependencies=guard)
|
||||
api_router.include_router(admin_user_behavior.router, prefix="/admin", tags=["admin-user-behavior"], dependencies=guard)
|
||||
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"], dependencies=guard)
|
||||
|
||||
Reference in New Issue
Block a user