parent
3b025a2eca
commit
638d9fe8f3
|
|
@ -0,0 +1,47 @@
|
|||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class Choice(BaseModel):
|
||||
index: int
|
||||
message: Message
|
||||
finish_reason: Optional[str] = None
|
||||
|
||||
|
||||
class Usage(BaseModel):
|
||||
prompt_tokens: int
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
|
||||
|
||||
class AIQuestionRequest(BaseModel):
|
||||
word: str
|
||||
question: str
|
||||
|
||||
|
||||
class AIAnswerResponse(BaseModel):
|
||||
id: str
|
||||
object: str
|
||||
created: int
|
||||
model: str
|
||||
choices: List[Choice]
|
||||
usage: Optional[Usage] = None
|
||||
|
||||
def get_answer(self) -> str:
|
||||
"""返回第一个回答的文本内容"""
|
||||
if self.choices and self.choices[0].message:
|
||||
return self.choices[0].message.content
|
||||
return ""
|
||||
|
||||
|
||||
class AIAnswerOut(BaseModel):
|
||||
word: str
|
||||
answer: str
|
||||
model: str
|
||||
tokens_used: Optional[int] = None
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import os
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.api.ai_assist import service
|
||||
from app.api.ai_assist.ai_schemas import AIAnswerResponse, AIAnswerOut, AIQuestionRequest
|
||||
from app.api.ai_assist.utils.redis_memory import get_chat_history, save_message, clear_chat_history
|
||||
from app.models import User
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
ai_router = APIRouter()
|
||||
|
||||
ZJU_AI_URL = 'https://chat.zju.edu.cn/api/ai/v1/chat/completions'
|
||||
AI_API_KEY = os.getenv("AI_ASSIST_KEY")
|
||||
MAX_USAGE_PER = 100
|
||||
|
||||
CHAT_TTL = 7200
|
||||
|
||||
|
||||
@ai_router.post("/exp")
|
||||
async def dict_exp(
|
||||
request: Request,
|
||||
Q: AIQuestionRequest,
|
||||
user: Tuple[User, Dict] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
|
||||
:param word:
|
||||
:param question: 不允许question为空调用
|
||||
:return:
|
||||
"""
|
||||
if user[0].token_usage > CHAT_TTL and not user[0].is_admin:
|
||||
raise HTTPException(status_code=400, detail="本月API使用量已超")
|
||||
|
||||
redis = request.app.state.redis
|
||||
|
||||
user_id = str(user[0].id)
|
||||
word = Q.word
|
||||
question = Q.question
|
||||
|
||||
await service.get_and_set_last_key(redis, word=word, user_id=user_id)
|
||||
|
||||
history = await get_chat_history(redis, user_id, word)
|
||||
|
||||
prompt = (
|
||||
f"用户正在学习词语「{word}」。"
|
||||
f"请回答与该词相关的问题:{question}\n"
|
||||
)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一位语言词典助手,回答要简洁、自然,适合初学者理解。只回答与词汇有关的问题。"},
|
||||
]
|
||||
messages.extend(history)
|
||||
messages.append(
|
||||
{"role": "user", "content": prompt}
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": "deepseek-r1-671b",
|
||||
"messages": messages,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {AI_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
resp = await client.post(ZJU_AI_URL, json=payload, headers=headers)
|
||||
|
||||
# 如果状态码不是200,抛异常
|
||||
if resp.status_code != 200:
|
||||
raise HTTPException(status_code=resp.status_code, detail=resp.text)
|
||||
|
||||
# 用 Pydantic 模型验证和解析返回结果
|
||||
ai_resp = AIAnswerResponse(**resp.json())
|
||||
|
||||
answer = ai_resp.get_answer()
|
||||
|
||||
await save_message(redis, user_id, word, "user", question)
|
||||
await save_message(redis, user_id, word, "assistant", answer)
|
||||
|
||||
return AIAnswerOut(
|
||||
word=word,
|
||||
answer=ai_resp.get_answer(),
|
||||
model=ai_resp.model,
|
||||
tokens_used=ai_resp.usage.total_tokens if ai_resp.usage else None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"AI调用失败: {str(e)}")
|
||||
|
||||
|
||||
@ai_router.post("/clear")
|
||||
async def clear_history(word: str, request: Request, user: Tuple[User, Dict] = Depends(get_current_user)):
|
||||
redis = request.app.state.redis
|
||||
user_id = user[0].id
|
||||
await clear_chat_history(redis, user_id, word)
|
||||
return {"msg": f"已清除 {word} 的聊天记录"}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from redis import Redis
|
||||
|
||||
from app.api.ai_assist.utils.redis_memory import clear_chat_history
|
||||
|
||||
CHAT_TTL = 7200
|
||||
|
||||
|
||||
async def get_and_set_last_key(redis: Redis, word: str, user_id: str):
|
||||
last_key = f"last_word:{user_id}"
|
||||
last_word = await redis.get(last_key)
|
||||
|
||||
# 如果上一次查的词和这次不同,就清空旧词的记录
|
||||
if last_word and last_word.decode() != word:
|
||||
await clear_chat_history(redis, user_id, last_word.decode())
|
||||
|
||||
# 更新当前词
|
||||
await redis.set(last_key, word, ex=CHAT_TTL)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
MAX_HISTORY = 6 # 每个用户保留最近3轮 (user+assistant)
|
||||
CHAT_TTL = 7200
|
||||
|
||||
|
||||
async def get_chat_history(redis, user_id: str, word: str) -> List[Dict]:
|
||||
"""
|
||||
从 Redis 获取历史消息
|
||||
"""
|
||||
key = f"chat:{user_id}:{word}"
|
||||
data = await redis.lrange(key, 0, -1)
|
||||
messages = [json.loads(d) for d in data]
|
||||
return messages[-MAX_HISTORY:] # 仅返回最近N条
|
||||
|
||||
|
||||
async def save_message(redis, user_id: str, word: str, role: str, content: str):
|
||||
"""
|
||||
保存单条消息到 Redis
|
||||
"""
|
||||
key = f"chat:{user_id}:{word}"
|
||||
msg = msg = json.dumps({"role": role, "content": content})
|
||||
await redis.rpush(key, msg)
|
||||
# 限制总长度
|
||||
await redis.ltrim(key, -MAX_HISTORY, -1)
|
||||
await redis.expire(key, CHAT_TTL)
|
||||
|
||||
|
||||
async def clear_chat_history(redis, user_id: str, word: str):
|
||||
"""
|
||||
删除某个用户针对某个词汇的全部聊天记录
|
||||
"""
|
||||
key = f"chat:{user_id}:{word}"
|
||||
await redis.delete(key)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.models import User, CommentFr, CommentJp
|
||||
from app.schemas.comment_schemas import CommentUpload
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
comment_router = APIRouter()
|
||||
|
||||
|
||||
@comment_router.post("/make-comment")
|
||||
async def new_word_comment(
|
||||
upload: CommentUpload,
|
||||
user: Tuple[User, dict] = Depends(get_current_user)
|
||||
) -> None:
|
||||
if upload.lang == "fr":
|
||||
await CommentFr.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
else:
|
||||
await CommentJp.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Literal, Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.models import User, CommentFr, CommentJp
|
||||
from app.schemas.comment_schemas import CommentUpload
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
word_comment_router = APIRouter()
|
||||
|
||||
@word_comment_router.post("/{lang}")
|
||||
async def create_word_comment(
|
||||
lang: Literal["jp", "fr"],
|
||||
upload: CommentUpload,
|
||||
user: Tuple[User, dict] = Depends(get_current_user)
|
||||
):
|
||||
if lang == "fr":
|
||||
await CommentFr.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
else:
|
||||
await CommentJp.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import os
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
"""
|
||||
# 背景
|
||||
你是一个人工智能助手,名字叫EduChat,是一个由华东师范大学开发的教育领域大语言模型。
|
||||
# 对话主题:作文指导
|
||||
## 作文指导主题的要求:
|
||||
EduChat你需要扮演一位经验丰富的语文老师,现在需要帮助一位学生审阅作文并给出修改建议。请按照以下步骤进行:
|
||||
整体评价:先对作文的整体质量进行简要评价,指出主要优点和需要改进的方向。
|
||||
亮点分析:具体指出作文中的亮点(如结构、描写、情感表达等方面的优点)。
|
||||
具体修改建议:针对作文中的不足,从以下几个方面提出具体修改建议,并给出修改后的示例:
|
||||
语言表达:是否生动、准确?有无冗余或重复?可以如何优化?
|
||||
细节描写:是否足够具体?能否加入更多感官描写(视觉、听觉、嗅觉、触觉等)使画面更立体?
|
||||
情感表达:情感是否自然?能否更深入或升华?
|
||||
结构布局:段落衔接是否自然?开头结尾是否呼应? (注意:每个建议点都要结合原文具体句子进行分析,并给出修改后的句子或段落作为示例)
|
||||
写作技巧提示:提供2-3条实用的写作技巧(如动态描写公式、感官交织法等),帮助学生举一反三。
|
||||
修改效果总结:简要说明按照建议修改后,作文会有哪些方面的提升(如文学性、情感层次、场景沉浸感等)。
|
||||
请用亲切、鼓励的语气进行点评,保持专业性同时让学生易于接受。
|
||||
"""
|
||||
|
||||
article_router = APIRouter()
|
||||
|
||||
ECNU_API_KEY = os.getenv("ECNU_TEACH_AI_KEY")
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ class CommentFr(Model):
|
|||
comment_word = fields.ForeignKeyField("models.WordlistFr", related_name="comments_fr")
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
supervised = fields.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
table = "comments_fr"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
from enum import Enum
|
||||
|
||||
import pandas as pd
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
from typing import Tuple, Type, TypeVar
|
||||
from tortoise.models import Model
|
||||
|
||||
from app.schemas.admin_schemas import PosEnumFr
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from typing import List, Tuple
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ class CommentSet(BaseModel):
|
|||
class CommentUpload(BaseModel):
|
||||
comment_word: str
|
||||
comment_content: str
|
||||
lang: Literal["fr", "jp"]
|
||||
# lang: Literal["fr", "jp"]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
7
main.py
7
main.py
|
|
@ -12,9 +12,10 @@ from app.api.redis_test import redis_test_router
|
|||
from app.api.search import dict_search
|
||||
from app.api.translator import translator_router
|
||||
from app.api.user.routes import users_router
|
||||
from app.api.word_comment.routes import word_comment_router
|
||||
from app.core.redis import init_redis, close_redis
|
||||
from app.utils.phone_encrypt import PhoneEncrypt
|
||||
from settings import TORTOISE_ORM
|
||||
from settings import ONLINE_SETTINGS
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
|
@ -42,7 +43,7 @@ app.add_middleware(
|
|||
|
||||
register_tortoise(
|
||||
app=app,
|
||||
config=TORTOISE_ORM,
|
||||
config=ONLINE_SETTINGS,
|
||||
)
|
||||
|
||||
app.include_router(users_router, tags=["User API"], prefix="/users")
|
||||
|
|
@ -55,5 +56,7 @@ app.include_router(translator_router, tags=["Translation API"])
|
|||
|
||||
app.include_router(ai_router, tags=["AI Assist API"], prefix="/ai_assist")
|
||||
|
||||
app.include_router(word_comment_router, tags=["Word Comment API"], prefix="/comment/word")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ from tkinter.scrolledtext import example
|
|||
import pandas as pd
|
||||
from tortoise import Tortoise, connections
|
||||
from tortoise.exceptions import MultipleObjectsReturned
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.models.fr import DefinitionFr, WordlistFr
|
||||
from settings import TORTOISE_ORM
|
||||
import app.models.signals
|
||||
|
||||
xlsx_name = "./DictTable_20250811.xlsx"
|
||||
xlsx_path = Path(xlsx_name)
|
||||
|
|
@ -68,7 +66,7 @@ async def import_def_fr(
|
|||
example = None if pd.isna(row.法语例句1) else str(row.法语例句1).strip()
|
||||
pos = None if pd.isna(row.词性1) else pos_process(str(row.词性1).strip())
|
||||
eng_exp = None if pd.isna(row.英语释义1) else str(row.英语释义1).strip()
|
||||
chi_exp = str(row[2]).strip()
|
||||
chi_exp = str(row[3]).strip()
|
||||
|
||||
# 去重:同一个词条不能有重复释义(同 pos + meaning)
|
||||
exists = await DefinitionFr.filter(
|
||||
|
|
|
|||
Loading…
Reference in New Issue