feat: add streaming learning assistant and knowledge base scaffolding

This commit is contained in:
刘金宝
2026-06-10 09:32:36 +08:00
parent f0cdc454b3
commit 89258ab448
31 changed files with 2021 additions and 330 deletions
+87
View File
@@ -0,0 +1,87 @@
from fastapi import APIRouter, Depends, File, Form, UploadFile
from sqlalchemy.orm import Session
from app.core.response import ApiResponse, ok
from app.core.user_context import UserContext, get_user_context
from app.db.session import get_db
from app.repositories.knowledge_base_repository import KnowledgeBaseRepository
from app.schemas.knowledge_admin import (
KnowledgeDocumentDetailResponse,
KnowledgeDocumentListResponse,
KnowledgeDocumentUploadResponse,
)
from app.services.document_ingestion_service import DocumentIngestionService
from app.services.knowledge_space_service import KnowledgeSpaceService
router = APIRouter()
@router.post("/documents/upload", response_model=ApiResponse[KnowledgeDocumentUploadResponse])
async def upload_knowledge_document(
file: UploadFile = File(..., description="PDF 文件"),
document_title: str | None = Form(default=None, description="文档标题"),
document_category: str = Form(default="textbook", description="文档分类:textbook/guideline/manual/other"),
version: str = Form(default="v1", description="文档版本"),
ctx: UserContext = Depends(get_user_context),
db: Session = Depends(get_db),
):
"""知识库上传:内容管理员上传 PDF,并触发机构知识库构建。"""
result = await DocumentIngestionService(db).upload_pdf(
ctx,
file,
document_title=document_title,
document_category=document_category,
version=version,
)
db.commit()
return ok(result)
@router.get("/documents", response_model=ApiResponse[KnowledgeDocumentListResponse])
def list_knowledge_documents(
ctx: UserContext = Depends(get_user_context),
db: Session = Depends(get_db),
):
"""知识库文档列表:内容管理员查看本机构已上传文档。"""
repo = KnowledgeBaseRepository(db)
KnowledgeSpaceService(repo).ensure_content_admin(ctx)
institution_id = KnowledgeSpaceService(repo).require_institution_id(ctx)
items = [_to_detail(item) for item in repo.list_documents(institution_id)]
return ok(KnowledgeDocumentListResponse(items=items))
@router.get("/documents/{document_id}", response_model=ApiResponse[KnowledgeDocumentDetailResponse])
def get_knowledge_document(
document_id: int,
ctx: UserContext = Depends(get_user_context),
db: Session = Depends(get_db),
):
"""知识库文档详情:按机构隔离返回 PDF 构建状态。"""
repo = KnowledgeBaseRepository(db)
KnowledgeSpaceService(repo).ensure_content_admin(ctx)
institution_id = KnowledgeSpaceService(repo).require_institution_id(ctx)
document = repo.get_document(document_id, institution_id)
if not document:
from app.core.exceptions import AppError
raise AppError("KNOWLEDGE_DOCUMENT_NOT_FOUND", "knowledge document not found", 404)
return ok(_to_detail(document))
def _to_detail(document) -> KnowledgeDocumentDetailResponse:
"""响应转换:把 ORM 文档对象转换为 API 文档详情。"""
return KnowledgeDocumentDetailResponse(
document_id=document.id,
institution_id=document.institution_id,
file_name=document.file_name,
document_title=document.document_title,
document_category=document.document_category,
version=document.version,
status=document.status,
parse_status=document.parse_status,
embedding_status=document.embedding_status,
chunk_count=document.chunk_count,
error_message=document.error_message,
created_at=getattr(document, "created_at", None),
updated_at=getattr(document, "updated_at", None),
)
+39
View File
@@ -0,0 +1,39 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from starlette.responses import StreamingResponse
from app.core.response import ApiResponse, ok
from app.core.user_context import UserContext, get_user_context
from app.db.session import get_db
from app.schemas.learning_assistant import LearningAssistantChatRequest, LearningAssistantChatResponse
from app.services.learning_assistant_service import LearningAssistantService
router = APIRouter()
@router.post("/chat", response_model=ApiResponse[LearningAssistantChatResponse], include_in_schema=False)
async def learning_assistant_chat(
payload: LearningAssistantChatRequest,
ctx: UserContext = Depends(get_user_context),
db: Session = Depends(get_db),
):
"""AI 学习助手调试接口:非流式返回回答,正式前端联调使用流式接口。"""
result = await LearningAssistantService(db).chat(ctx, payload)
db.commit()
return ok(result)
@router.post("/chat/stream", response_class=StreamingResponse)
async def learning_assistant_stream_chat(
payload: LearningAssistantChatRequest,
ctx: UserContext = Depends(get_user_context),
db: Session = Depends(get_db),
):
"""AI 学习助手流式问答:返回 retrieval_done、answer_delta、answer_done 事件。"""
stream = LearningAssistantService(db).stream_chat(ctx, payload)
db.commit()
return StreamingResponse(
stream,
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
)
+3 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api import agent, auth, cases, evaluations, sessions, teaching, training_config
from app.api import agent, auth, cases, evaluations, knowledge_admin, learning_assistant, sessions, teaching, training_config
api_router = APIRouter()
api_router.include_router(agent.router, tags=["agent"])
@@ -10,3 +10,5 @@ api_router.include_router(training_config.router, prefix="/training-config", tag
api_router.include_router(sessions.router, prefix="/sessions", tags=["sessions"])
api_router.include_router(teaching.router, prefix="/teaching", tags=["teaching"])
api_router.include_router(evaluations.router, prefix="/evaluations", tags=["evaluations"])
api_router.include_router(knowledge_admin.router, prefix="/knowledge-admin", tags=["knowledge-admin"])
api_router.include_router(learning_assistant.router, prefix="/learning-assistant", tags=["learning-assistant"])