feat: 切换Agent自主知识工具检索链路

This commit is contained in:
2026-07-11 19:14:17 +08:00
parent d2d43de6d3
commit 6cadca8c8e
14 changed files with 863 additions and 62 deletions

View File

@@ -0,0 +1,203 @@
from __future__ import annotations
import json
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.orm import Session
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.knowledge import (
HumanAttentionHistory,
HumanAttentionRecord,
KnowledgeRetrievalCandidate,
KnowledgeRetrievalLog,
)
from app.schemas.knowledge import AttentionUpdateRequest
from app.services.admin_service import OperationLogService
router = APIRouter()
@router.get("/retrieval-log/list")
def retrieval_logs(
sessionId: int | None = Query(default=None),
knowledgeId: int | None = Query(default=None),
knowledgeCalled: bool | None = Query(default=None),
statusValue: str = Query(default="", alias="status"),
attentionCreated: bool | None = Query(default=None),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(KnowledgeRetrievalLog).order_by(KnowledgeRetrievalLog.id.desc()).limit(300)
if sessionId is not None:
query = query.where(KnowledgeRetrievalLog.session_id == sessionId)
if knowledgeId is not None:
query = query.where(KnowledgeRetrievalLog.selected_knowledge_ids.like(f"%{knowledgeId}%"))
if knowledgeCalled is not None:
query = query.where(KnowledgeRetrievalLog.knowledge_called == (1 if knowledgeCalled else 0))
if statusValue:
query = query.where(KnowledgeRetrievalLog.status == statusValue)
if attentionCreated is not None:
query = query.where(KnowledgeRetrievalLog.attention_created == (1 if attentionCreated else 0))
return api_success([_retrieval_dict(item, detail=False) for item in db.scalars(query).all()])
@router.get("/retrieval-log/{log_id}")
def retrieval_detail(
log_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
log = db.get(KnowledgeRetrievalLog, log_id)
if log is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="检索日志不存在")
candidates = db.scalars(
select(KnowledgeRetrievalCandidate)
.where(KnowledgeRetrievalCandidate.retrieval_log_id == log.id)
.order_by(KnowledgeRetrievalCandidate.selected.desc(), KnowledgeRetrievalCandidate.id)
).all()
data = _retrieval_dict(log, detail=True)
data["candidates"] = [
{
"id": item.id,
"knowledgeId": item.knowledge_id,
"versionId": item.version_id,
"chunkId": item.chunk_id,
"sectionId": item.section_id,
"lexicalScore": item.lexical_score,
"rerankScore": item.rerank_score,
"selected": item.selected == 1,
"discardReason": item.discard_reason,
}
for item in candidates
]
return api_success(data)
@router.get("/attention/list")
def attention_list(
priority: str = Query(default=""),
statusValue: str = Query(default="", alias="status"),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
query = select(HumanAttentionRecord).order_by(HumanAttentionRecord.id.desc()).limit(300)
if priority:
query = query.where(HumanAttentionRecord.priority == priority)
if statusValue:
query = query.where(HumanAttentionRecord.status == statusValue)
return api_success([_attention_dict(item) for item in db.scalars(query).all()])
@router.put("/attention/{attention_id}")
def update_attention(
attention_id: int,
payload: AttentionUpdateRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
item = db.get(HumanAttentionRecord, attention_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="人工关注记录不存在")
previous = item.status
item.status = payload.status
item.handler_id = current_admin.id
item.handler_note = payload.note
db.add_all(
[
item,
HumanAttentionHistory(
attention_id=item.id,
from_status=previous,
to_status=payload.status,
note=payload.note,
operated_by=current_admin.id,
),
]
)
OperationLogService.write(db, admin_id=current_admin.id, module="attention", action="update", target_id=item.id)
db.commit()
return api_success(_attention_dict(item))
@router.delete("/attention/{attention_id}")
def delete_attention(
attention_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
item = db.get(HumanAttentionRecord, attention_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="人工关注记录不存在")
histories = db.scalars(
select(HumanAttentionHistory).where(HumanAttentionHistory.attention_id == item.id)
).all()
for history in histories:
db.delete(history)
db.delete(item)
OperationLogService.write(db, admin_id=current_admin.id, module="attention", action="delete", target_id=attention_id)
db.commit()
return api_success()
def _retrieval_dict(item: KnowledgeRetrievalLog, *, detail: bool) -> dict:
data = {
"id": item.id,
"sessionId": item.session_id,
"messageId": item.message_id,
"userId": item.user_id,
"question": item.question,
"knowledgeCalled": item.knowledge_called == 1,
"selectedKnowledgeIds": item.selected_knowledge_ids,
"queryTerms": _json(item.query_terms),
"finalSectionIds": item.final_section_ids,
"status": item.status,
"toolCostMs": item.tool_cost_ms,
"totalCostMs": item.total_cost_ms,
"errorMessage": item.error_message,
"attentionCreated": item.attention_created == 1,
"createdAt": item.created_at,
}
if detail:
data.update(
{
"catalogSnapshot": _json(item.catalog_snapshot),
"catalogHash": item.catalog_hash,
"safetyRuleVersion": item.safety_rule_version,
"toolTrace": _json(item.tool_trace),
"discardedConflicts": _json(item.discarded_conflicts),
"finalAnswer": item.final_answer,
}
)
return data
def _attention_dict(item: HumanAttentionRecord) -> dict:
return {
"id": item.id,
"sessionId": item.session_id,
"messageId": item.message_id,
"userId": item.user_id,
"triggerMessage": item.trigger_message,
"problemSummary": item.problem_summary,
"triggerReason": item.trigger_reason,
"priority": item.priority,
"status": item.status,
"handlerId": item.handler_id,
"handlerNote": item.handler_note,
"createdAt": item.created_at,
"updatedAt": item.updated_at,
}
def _json(raw: str | None):
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw

View File

@@ -291,6 +291,31 @@ def migration_summary(
return api_success({"total": len(rows), "counts": counts, "items": [_knowledge_detail(item) for item in rows]})
@router.post("/knowledge-migration/run")
async def run_migration(
knowledgeId: int | None = Query(default=None),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
_require_super_admin(current_admin)
query = select(Knowledge).where(Knowledge.lifecycle_status == "active")
if knowledgeId is not None:
query = query.where(Knowledge.id == knowledgeId)
rows = db.scalars(query.order_by(Knowledge.id)).all()
results = []
for knowledge in rows:
try:
job = await KnowledgePipelineService.synchronize(
db, knowledge, admin_id=current_admin.id, mode="review"
)
results.append({"knowledgeId": knowledge.id, "ok": True, "job": _job_dict(job)})
except Exception as exc:
results.append({"knowledgeId": knowledge.id, "ok": False, "error": str(exc)})
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge_migration", action="run", target_id=knowledgeId)
db.commit()
return api_success({"total": len(results), "success": sum(1 for item in results if item["ok"]), "results": results})
def _require_super_admin(admin: Admin) -> None:
if admin.role is not None and admin.role.code != "super_admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅超级管理员可以执行此操作")

View File

@@ -19,6 +19,7 @@ from app.schemas.admin import (
)
from app.services.admin_service import OperationLogService
from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_agent_service import KnowledgeAgentService
from app.services.knowledge_service import KnowledgeScope
from app.services.model_service import ModelClientService
from app.services.rag_service import RagResult
@@ -60,7 +61,7 @@ def reset_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(g
@router.post("/agent/debug")
def debug_agent(
async def debug_agent(
payload: AgentDebugRequest,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
@@ -69,10 +70,21 @@ def debug_agent(
if model is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
scopes = _agent_knowledge_scopes(db, payload.knowledgeIds)
chunks = FeishuKnowledgeService.retrieve(payload.question, scopes, db)
prompt = _build_agent_debug_prompt(payload.promptContent, payload.question, chunks)
rag_result = RagResult(question=payload.question, knowledge_scopes=scopes, chunks=chunks, prompt=prompt)
rag_result = await KnowledgeAgentService.build_result(
db,
question=payload.question,
version_overrides=payload.knowledgeVersions,
preview_knowledge_ids=payload.knowledgeIds,
)
rag_result = RagResult(
question=rag_result.question,
knowledge_scopes=rag_result.knowledge_scopes,
chunks=rag_result.chunks,
prompt=f"{payload.promptContent.strip()}\n\n{rag_result.prompt}",
allow_general_knowledge=rag_result.allow_general_knowledge,
retrieval_log_id=rag_result.retrieval_log_id,
tool_trace=rag_result.tool_trace,
)
result = ModelClientService.debug_model(
model,
rag_result,
@@ -85,6 +97,8 @@ def debug_agent(
"max_token": payload.maxToken,
},
)
result["retrievalTrace"] = rag_result.tool_trace or []
result["retrievalLogId"] = rag_result.retrieval_log_id
OperationLogService.write(db, admin_id=current_admin.id, module="agent", action="debug", target_id=model.id)
db.commit()
return api_success(result)

View File

@@ -4,6 +4,7 @@ from fastapi import APIRouter
from app.api import (
admin_auth,
admin_agent_records,
admin_dashboard,
admin_knowledge,
admin_knowledge_lifecycle,
@@ -22,6 +23,7 @@ api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(user.router, prefix="/user", tags=["user"])
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"])
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"])