快捷问题问答的部分

main
zc 3 months ago
parent 91c18d91f0
commit 43fbda1a50
  1. 62
      app/api/chat/ai/chat_router.py
  2. 42
      app/api/chat/ai/chat_service.py
  3. 3
      app/api/v1/upload/upload.py
  4. 4
      app/core/init_app.py
  5. 2
      web/src/api/index.js
  6. 44
      web/src/views/system/question/index.vue

@ -2,14 +2,15 @@ from fastapi import APIRouter, HTTPException
from starlette.requests import Request
from fastapi.responses import StreamingResponse
from typing import AsyncGenerator
from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream
from typing import AsyncGenerator, List
from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream, handle_quick_question
from app.models.ChatIn import ChatIn
from app.models.quick_question import QuickQuestion
from pydantic import BaseModel
import hmac
import hashlib
import time
import json
from pydantic import BaseModel
from app.settings.config import settings
router = APIRouter()
@ -43,23 +44,42 @@ async def h5_chat_stream(request: Request, inp: ChatIn):
conversation_history_str = await redis_client.get(conversation_history_key)
conversation_history = json.loads(conversation_history_str) if conversation_history_str else []
# 分类阶段(保留同步调用)
cat = await classify(inp.message)
# 获取开启的前4个问题(包含标题和内容)
questions = await QuickQuestion.filter(status="0").order_by("order_num").limit(4).values("title", "content")
question_titles = [q["title"] for q in questions]
# 检查消息是否在问题列表中
is_quick_question = inp.message in question_titles
# 分类阶段(如果不是快捷问题才执行)
cat = None
if not is_quick_question:
cat = await classify(inp.message)
async def content_stream() -> AsyncGenerator[str, None]:
nonlocal conversation_history
try:
if cat == "游玩判断":
spot = await extract_spot(inp.message)
data = await query_flow(request, spot)
async for chunk in gen_markdown_stream(inp.message, data, inp.language, conversation_history):
if is_quick_question:
# 找到对应的问题内容
question_content = next(q["content"] for q in questions if q["title"] == inp.message)
# 处理快捷问题,传递content
async for chunk in handle_quick_question(inp, question_content):
yield chunk
else:
async for chunk in ai_chat_stream(inp, conversation_history):
yield chunk
# 原来的逻辑
if cat == "游玩判断":
spot = await extract_spot(inp.message)
data = await query_flow(request, spot)
async for chunk in gen_markdown_stream(inp.message, data, inp.language, conversation_history):
yield chunk
else:
async for chunk in ai_chat_stream(inp, conversation_history):
yield chunk
# 将更新后的对话历史存回 Redis,并设置过期时间
await redis_client.setex(conversation_history_key, CONVERSATION_EXPIRE_TIME, json.dumps(conversation_history))
# 只有非快捷问题才保存对话历史
if not is_quick_question:
await redis_client.setex(conversation_history_key, CONVERSATION_EXPIRE_TIME, json.dumps(conversation_history))
except Exception as e:
print(f"Error in content_stream: {e}")
raise
@ -102,4 +122,20 @@ def verify_signature(data: dict, sign: str) -> bool:
def verify_timestamp(timestamp: int) -> bool:
current_timestamp = int(time.time() * 1000)
return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000
return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000
# 定义获取问题的响应模型
class QuestionResponse(BaseModel):
id: int
title: str
class Config:
orm_mode = True
@router.get("/getQuestion", summary="获取开启的前4个问题")
async def get_question():
# 查询状态为正常(0)的问题,按order_num正序排序,取前4条
questions = await QuickQuestion.filter(status="0").order_by("order_num").limit(4).values("id", "title")
return questions

@ -320,4 +320,44 @@ async def query_flow(request: Request, spot: str) -> str:
except Exception as e:
print(f"[Redis] 写缓存失败: {e}")
return result
return result
async def handle_quick_question(inp: ChatIn, question_content: str) -> AsyncGenerator[str, None]:
chat_prompt = f"""
你是一个专门格式化内容的AI助手
负责将接收到的包含html标签内容进行格式化要求是将能够转换成markdown语法的内容中的html标签转换成markdown语法
不能转换的保留html标签不能修改内容仅格式化标签
"""
# 只包含系统提示和问题内容,不包含历史记录
messages = [
{"role": "system", "content": chat_prompt},
{"role": "user", "content": question_content}
]
full_response = ""
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
# 使用异步方式处理同步流
import asyncio
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
yield delta.content
import sys
sys.stdout.flush()
# 短暂让出控制权,避免阻塞
await asyncio.sleep(0)
# 添加结束标记
yield " "
except Exception as e:
error_msg = f"当前访问人数过多,请稍后重试: {str(e)}"
print(error_msg)
yield error_msg
# 不保存快捷问题的对话历史
print("Quick question handling finished.")

@ -1,4 +1,4 @@
# app/api/v1/upload/upload.py
import os
import uuid
from fastapi import APIRouter, File, UploadFile
@ -13,6 +13,7 @@ UPLOAD_DIR = os.path.join(settings.BASE_DIR, 'web', 'public', 'resource')
@router.post('/image', summary='上传图片')
async def upload_image(file: UploadFile = File(...)):
print(file.filename)
try:
# 确保目录存在
os.makedirs(os.path.join(UPLOAD_DIR, 'images'), exist_ok=True)

@ -46,7 +46,9 @@ def make_middlewares():
"/api/v1/base/access_token",
"/docs",
"/openapi.json",
"/api/model/"
"/api/model/",
"/api/v1/upload/image",
"/api/v1/upload/video"
],
),
]

@ -45,4 +45,6 @@ export default {
createQuestion: (data = {}) => request.post('/question/create', data),
updateQuestion: (data = {}) => request.post('/question/update', data),
deleteQuestion: (params = {}) => request.delete('/question/delete', { params }),
// 上传图片
uploadImages: (data = {}) => request.post('/upload/image', data)
}

@ -91,6 +91,7 @@ import { h, onMounted, ref, resolveDirective, withDirectives, onUnmounted, watch
import {
NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch
} from 'naive-ui'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
@ -103,9 +104,8 @@ import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables'
import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue'
import { getToken } from '@/utils/auth/token'
import { request } from '@/utils/http'; // request
// defineOptions script setup
defineOptions({ name: '快捷问题管理' })
//
@ -116,34 +116,34 @@ const editorConfig = ref({
placeholder: '请输入问题内容...',
MENU_CONF: {
uploadImage: {
server: '/api/v1/upload/image',
fieldName: 'file',
maxFileSize: 2 * 1024 * 1024,
headers: {
'token': getToken()
},
//
customInsert(res, insertFn) {
if (res.code === 200 && res.data && res.data.url) {
insertFn(res.data.url);
} else {
ElMessage.error(`上传失败: ${res.msg || '未知错误'}`);
customUpload: async (file, insertFn) => {
try {
const formData = new FormData();
formData.append('file', file);
console.log('FormData内容:', formData.get('file'));
console.log('准备发送请求...');
const res = await api.uploadImages(formData);
console.log('请求响应:', res);
if (res.code === 200 && res.data && res.data.url) {
insertFn(res.data.url);
} else {
window.$message.error('图片上传失败: ' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('上传失败:', error);
window.$message.error('图片上传失败: ' + error.message);
}
}
},
uploadVideo: {
server: '/api/v1/upload/video',
fieldName: 'file',
maxFileSize: 100 * 1024 * 1024,
headers: {
'token': getToken()
},
//
customInsert(res, insertFn) {
if (res.code === 200 && res.data && res.data.url) {
insertFn(res.data.url);
} else {
ElMessage.error(`上传失败: ${res.msg || '未知错误'}`);
// NMessage
window.$message.error(`上传失败: ${res.msg || '未知错误'}`);
}
}
}

Loading…
Cancel
Save