diff --git a/.env b/.env index 178690f..bf0b75e 100644 --- a/.env +++ b/.env @@ -7,8 +7,8 @@ OCR_API_URL=https://paddleocr.aistudio-app.com/api/v2/ocr/jobs OCR_API_TOKEN=df7dcc85a5c3c9d64e421f353d11d13ec45512f6 OCR_MODEL=PaddleOCR-VL-1.6 -MODEL_NAME=deepseek-v4-flash -MODEL_BASE_URL=https://api.deepseek.com -MODEL_API_KEY=sk-0b237d41f6bc44fc9732ea66bd7eade0 +MODEL_NAME="glm-5.2" +MODEL_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" +MODEL_API_KEY="sk-52bcd98e9c1d45908437c4e8706eefff" DATABASE_URL=postgresql+asyncpg://agent:medical%402019@60.247.145.200:5432/study diff --git a/main.py b/main.py index 28ead64..27f6f09 100644 --- a/main.py +++ b/main.py @@ -8,8 +8,10 @@ from starlette.responses import StreamingResponse from database import get_db from schemas.agent import QueryRequest from schemas.library import LibraryFileRequest, LibraryFileResponse +from schemas.mistake import MistakeResponse +from schemas.practice import PracticeQuestionResponse, PracticeCheckResult, PracticeRecordResponse +from service import library_service, practice_service, mistake_service from service.agent_service import query_agent -from service.library_service import add_file, get_file, list_files, edit_file from storage import upload_rustfs from ocr import ocr_from_url @@ -39,20 +41,40 @@ def generate(query: QueryRequest): @app.get("/library/files", response_model=List[LibraryFileResponse]) -async def list_library_file(db: AsyncSession = Depends(get_db)): - return await list_files(db) +async def list_files(db: AsyncSession = Depends(get_db)): + return await library_service.list_files(db) @app.get("/library/files/{id}", response_model=LibraryFileResponse) -async def get_library_file(id: int, db: AsyncSession = Depends(get_db)): - return await get_file(db, id) +async def get_file(id: int, db: AsyncSession = Depends(get_db)): + return await library_service.get_file(db, id) @app.post("/library/files", response_model=bool) -async def add_library_file(data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): - return await add_file(db, data) +async def add_file(data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): + return await library_service.add_file(db, data) @app.put("/library/files/{id}", response_model=bool) -async def edit_library_file(id: int, data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): - return await edit_file(db, id, data) +async def edit_file(id: int, data: LibraryFileRequest, db: AsyncSession = Depends(get_db)): + return await library_service.edit_file(db, id, data) + + +@app.get("/practice/files/{id}", response_model=List[PracticeQuestionResponse]) +async def get_question(id: int, db: AsyncSession = Depends(get_db)): + return await practice_service.get_question(db, id) + + +@app.get("/practice/record/{id}", response_model=List[PracticeRecordResponse]) +async def get_record(id: int, db: AsyncSession = Depends(get_db)): + return await practice_service.get_record(db, id) + + +@app.post("/practice/check/{id}", response_model=List[PracticeCheckResult]) +async def check_practice(id: int, data: List[PracticeQuestionResponse], db: AsyncSession = Depends(get_db)): + return await practice_service.check_practice(db, id, data) + + +@app.get("/mistake", response_model=List[MistakeResponse]) +async def list_mistakes(db: AsyncSession = Depends(get_db)): + return await mistake_service.list_mistakes(db) diff --git a/models/library.py b/models/library.py index cf1b25f..13b6686 100644 --- a/models/library.py +++ b/models/library.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, BigInteger, String, Text, TIMESTAMP, Integer, ForeignKey +from sqlalchemy import Column, BigInteger, String, Text, Integer, ForeignKey from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship @@ -19,6 +19,18 @@ class LibraryFile(AuditBase): cascade="all, delete-orphan" ) + practice_record = relationship( + "PracticeRecord", + back_populates="library_file", + cascade="all, delete-orphan" + ) + + mistake = relationship( + "Mistake", + back_populates="library_file", + cascade="all, delete-orphan" + ) + class LibraryFileQuestion(AuditBase): library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) @@ -32,3 +44,38 @@ class LibraryFileQuestion(AuditBase): "LibraryFile", back_populates="questions" ) + + mistake = relationship( + "Mistake", + back_populates="library_file_question", + cascade="all, delete-orphan" + ) + + +class PracticeRecord(AuditBase): + library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) + + total_count = Column(Integer, nullable=False) + correct_count = Column(Integer, nullable=False) + wrong_count = Column(Integer, nullable=False) + + library_file = relationship( + "LibraryFile", + back_populates="practice_record", + ) + +class Mistake(AuditBase): + library_file_id = Column(BigInteger, ForeignKey("library_file.id"), nullable=False) + question_id = Column(BigInteger, ForeignKey("library_file_question.id"), nullable=False) + + wrong_count = Column(Integer, nullable=False) + + library_file = relationship( + "LibraryFile", + back_populates="mistake", + ) + + library_file_question = relationship( + "LibraryFileQuestion", + back_populates="mistake", + ) diff --git a/schemas/library.py b/schemas/library.py index 9b93a31..e0c6fc8 100644 --- a/schemas/library.py +++ b/schemas/library.py @@ -1,5 +1,3 @@ -from datetime import datetime - from pydantic import BaseModel from typing import List diff --git a/schemas/mistake.py b/schemas/mistake.py new file mode 100644 index 0000000..bf82671 --- /dev/null +++ b/schemas/mistake.py @@ -0,0 +1,18 @@ +from typing import List + +from pydantic import BaseModel + + +class MistakeResponse(BaseModel): + id: int + subject: str + module: str + name: str + type: str + question: str + options: List[str] + answer: List[str] + wrongCount: int + + class Config: + from_attributes = True diff --git a/schemas/practice.py b/schemas/practice.py new file mode 100644 index 0000000..dd97cf9 --- /dev/null +++ b/schemas/practice.py @@ -0,0 +1,38 @@ +from typing import List + +from pydantic import BaseModel + +from schemas.library import QuestionRequest + + +class PracticeRecordRequest(BaseModel): + libraryFileId: int + totalCount: int + correctCount: int + wrongCount: int + + +class PracticeRecordResponse(PracticeRecordRequest): + id: int + createTime: str + + class Config: + from_attributes = True + + +class PracticeQuestionResponse(QuestionRequest): + id: int + select: List[str] + + class Config: + from_attributes = True + + +class PracticeCheckResult(BaseModel): + id: int + answer: List[str] + select: List[str] + isCorrect: bool + + class Config: + from_attributes = True \ No newline at end of file diff --git a/service/library_service.py b/service/library_service.py index d242dc0..bbfbd55 100644 --- a/service/library_service.py +++ b/service/library_service.py @@ -121,7 +121,7 @@ async def get_file(db: AsyncSession, file_id: int) -> LibraryFileResponse: type=q.type, question=q.question, options=q.options, - answer=q.answer, + answer=q.answer ) for q in file.questions ], diff --git a/service/mistake_service.py b/service/mistake_service.py new file mode 100644 index 0000000..a48222c --- /dev/null +++ b/service/mistake_service.py @@ -0,0 +1,68 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from models.library import Mistake, LibraryFile, LibraryFileQuestion +from schemas.mistake import MistakeResponse + + +async def list_mistakes(db: AsyncSession) -> list[MistakeResponse]: + stmt = ( + select( + Mistake.id, + LibraryFile.subject, + LibraryFile.module, + LibraryFile.name, + LibraryFileQuestion.type, + LibraryFileQuestion.question, + LibraryFileQuestion.options, + LibraryFileQuestion.answer, + Mistake.wrong_count, + ) + .join( + LibraryFileQuestion, + Mistake.question_id == LibraryFileQuestion.id, + ) + .join( + LibraryFile, + Mistake.library_file_id == LibraryFile.id, + ) + ) + stmt = stmt.order_by(Mistake.create_time.desc()) + result = await db.execute(stmt) + rows = result.mappings().all() + + return [ + MistakeResponse( + id=row["id"], + subject=row["subject"], + module=row["module"], + name=row["name"], + type=row["type"], + question=row["question"], + options=row["options"], + answer=row["answer"], + wrongCount=row["wrong_count"], + ) + for row in rows + ] + + +async def update_mistake(db: AsyncSession, library_file_id: int, question_id: int): + stmt = select(Mistake).where( + Mistake.library_file_id == library_file_id, + Mistake.question_id == question_id, + ) + result = await db.execute(stmt) + wrong = result.scalar_one_or_none() + + if wrong: + wrong.wrong_count += 1 + else: + wrong = Mistake( + library_file_id=library_file_id, + question_id=question_id, + wrong_count=1 + ) + db.add(wrong) + + await db.flush() diff --git a/service/practice_service.py b/service/practice_service.py new file mode 100644 index 0000000..641ece9 --- /dev/null +++ b/service/practice_service.py @@ -0,0 +1,107 @@ +from typing import List + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from models.library import LibraryFile, PracticeRecord +from schemas.practice import PracticeQuestionResponse, PracticeRecordRequest, PracticeCheckResult, \ + PracticeRecordResponse +from service.library_service import get_file +from service.mistake_service import update_mistake + + +async def get_record(db: AsyncSession, library_file_id: int) -> List[PracticeRecordResponse]: + stmt = select(PracticeRecord).where(PracticeRecord.library_file_id == library_file_id).order_by( + PracticeRecord.create_time.desc()) + records = (await db.execute(stmt)).scalars().all() + + return [ + PracticeRecordResponse( + id=record.id, + libraryFileId=record.library_file_id, + totalCount=record.total_count, + correctCount=record.correct_count, + wrongCount=record.wrong_count, + createTime=record.create_time.strftime("%Y-%m-%d %H:%M:%S"), + ) + for record in records + ] + + +async def get_question(db: AsyncSession, file_id: int) -> List[PracticeQuestionResponse]: + stmt = ( + select(LibraryFile) + .options(selectinload(LibraryFile.questions)) + .where(LibraryFile.id == file_id) + ) + result = await db.execute(stmt) + file = result.scalar_one_or_none() + + return [ + PracticeQuestionResponse( + id=q.id, + type=q.type, + question=q.question, + options=q.options, + answer=[], + select=[] + ) + for q in file.questions + ] + + +async def check_practice(db: AsyncSession, + library_file_id: int, + questions: List[PracticeQuestionResponse]) -> List[PracticeCheckResult]: + library_file = await get_file(db, library_file_id) + db_questions = library_file.questions + check_results = [] + + for user_q in questions: + db_q = next((q for q in db_questions if q.id == user_q.id), None) + if not db_q: + continue + + is_correct = set(user_q.select) == set(db_q.answer) + + if not is_correct: + await update_mistake( + db=db, + library_file_id=library_file_id, + question_id=user_q.id, + ) + + check_results.append(PracticeCheckResult( + id=user_q.id, + answer=db_q.answer, + select=user_q.select, + isCorrect=is_correct, + )) + + correct_count = sum(1 for r in check_results if r.isCorrect) + await add_practice_record( + db, + PracticeRecordRequest( + libraryFileId=library_file_id, + totalCount=len(check_results), + correctCount=correct_count, + wrongCount=len(check_results) - correct_count, + ), + ) + + return check_results + + +async def add_practice_record(db: AsyncSession, data: PracticeRecordRequest): + record = PracticeRecord( + library_file_id=data.libraryFileId, + total_count=data.totalCount, + correct_count=data.correctCount, + wrong_count=data.wrongCount, + ) + + db.add(record) + await db.commit() + + return True