zc 3 months ago
parent e8641454fd
commit f30e00fc51
  1. 2
      app/api/__init__.py
  2. 115
      app/api/chat/ai/chat_service.py
  3. 6
      app/api/upload/__init__.py
  4. 4
      app/api/v1/__init__.py
  5. 0
      app/api/v1/upload/__init__.py
  6. 0
      app/api/v1/upload/upload.py
  7. 2
      web/.env.development
  8. 2
      web/build/constant.js
  9. 47
      web/src/views/system/question/index.vue
  10. 5
      web/vite.config.js

@ -1,13 +1,11 @@
from fastapi import APIRouter from fastapi import APIRouter
from .chat import chat_router from .chat import chat_router
from .upload import upload_api_router
from .v1 import v1_router from .v1 import v1_router
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(v1_router, prefix="/v1") api_router.include_router(v1_router, prefix="/v1")
api_router.include_router(chat_router, prefix="") api_router.include_router(chat_router, prefix="")
api_router.include_router(upload_api_router, prefix="")
__all__ = ["api_router"] __all__ = ["api_router"]

@ -1,4 +1,6 @@
import os, openai, aiomysql import os, openai, aiomysql
import random
from openai import OpenAI from openai import OpenAI
from dotenv import load_dotenv from dotenv import load_dotenv
import redis.asyncio as redis import redis.asyncio as redis
@ -83,20 +85,73 @@ EXTRACT_PROMPT = """你是一名景区名称精准匹配助手。用户的问题
大慈阁""" 大慈阁"""
# 客流查询后回答的提示词 # 客流查询后回答的提示词
ANSWER_PROMPT = """生成一份人性化的出行指南。输出结构要求如下,使用生活化语言表达,注意不可依据想象生成未经查询的客流数据: ANSWER_PROMPT = """
首行为标题**景区名称**游览建议景区名称要显示正确完整 **景区游览指南生成要求**
**实时客流**展示在园人数=进入人数-离开人数若为负取绝对值超出最大承载量时不显示具体人数仅显示舒适度等级展示舒适度等级进入和离开人数瞬时承载量不展示承载率仅以舒适度等级汉字形式体现不展示百分比并用简短1-2句话描述舒适度等级代表的游览体验如游客较少游览体验轻松根据承载率范围输出等级并用 HTML 字体颜色标签展示舒适度等级<30% 为舒适绿色热门景点基本不排队通道畅通拍照无遮挡30%-50% 为较舒适蓝色热门景点排队 5-10 分钟通道不挤拍照等待短50%-70% 为一般黄色热门景点排队 15-30 分钟部分通道挤拍照需等70%-90% 为较拥挤橙色**热门景点排队 30-60 分钟通道拥挤拍照需错峰座位紧张>90% 为拥挤红色热门景点排队超 60 分钟通道拥堵拍照困难可能限流 <font color="green">舒适</font> <font color="blue">较舒适</font> <font color="yellow">一般</font> <font color="orange">较拥挤</font> <font color="red">拥挤</font>不要加括号或冗余说明仅展示彩色词语
**周边停车指南**展示停车场名称距景区距离步行所需时间当前余位收费标准每项数据为一行收费标准需提示具体以现场公示为准 输入数据格式
**出行与游览建议**是否前往强烈推荐/推荐/谨慎考虑/暂缓前往用生活化建议替代机械的等级描述强烈推荐可表述为现在正是游览的好时机馆内人少舒适无需排队等候可以随到随游享受宁静的观展氛围当较拥挤或拥挤时显示错峰建议如当日16:00后或次日8:30交通方式停车场充足时推荐自驾紧张建议打车或备用停车场已满建议使用公共交通如XX路公交路线建议当较拥挤或拥挤时则推荐反常规路线或人少区域建议游览时长较舒适多10%一般多20%较拥挤/拥挤多30%-50%不要直接显示百分比需提出具体时长如1小时以上替代方案当较拥挤或拥挤时推荐1-2个周边承载率低的景区并说明优势 - 用户问题{msg}
**温馨提示**如安全提醒天气影响等影响游客游览的内容语言风格亲切如朋友交谈"""记得""推荐"等暖心词汇禁用机械术语**加粗**突出关键建议加入表情符号增加亲和力不展示原始字段数据结尾不输出数据时间仅加提示动态信息请以现场为准祝您旅途愉快 - 查询到的数据{data}包含景区名称在园人数最大承载量进出人数等
若未找到景区的信息时生成景区通用游览建议使用以下输出结构 - 输出语言{language}
首行为标题**景区名称** 游览建议景区名称要显示正确完整
**实时客流**仅显示提示暂时无法获取实时客流数据您仍可参考以下出行建议 输出要求
**出行与游览建议**交通方式列出几种常规交通方式并推荐大众选择较多的出行方式简单描述优势和注意事项
游览路线描述游览景区常规路线和游览重点突出表现景区特色 1. 标题格式
游览时间显示常规参观时间旺季需多预留30%不要直接显示百分比需提出具体时长如1小时以上 **完整景区名称**游览建议
附近景区推荐推荐1-2个周边景区并说明景区特点
**温馨提示**如安全提醒天气影响等影响游客游览的内容语言风格亲切如朋友交谈"""记得""推荐"等暖心词汇禁用机械术语**加粗**突出关键建议加入表情符号增加亲和力不展示原始字段数据结尾不输出数据时间仅加提示动态信息请以现场为准祝您旅途愉快""" 2. 实时客流展示
- 计算当前在园人数进入人数-离开人数的绝对值
- 计算承载率在园人数/最大承载量
- 按以下标准显示舒适度等级
<30%<font color="green">舒适</font>
30%-50%<font color="blue">较舒适</font>
50%-70%<font color="blue">一般</font>
70%-90%<font color="blue">较拥挤</font>
>90%<font color="red">拥挤</font>
- 附加简短体验描述1-2
3. 周边停车指南
- 停车场名称
- 距离
- 步行时间分钟
- 当前余位/总车位
- 收费标准注明"具体以现场公示为准"
4. 出行与游览建议
- 推荐程度强烈推荐/推荐/谨慎考虑/暂缓前往
- 具体建议包含错峰时间交通方式路线建议
- 建议游览时长根据舒适度调整
- 替代景区推荐当拥挤时
5. 温馨提示
- 安全提醒
- 天气影响
- 其他注意事项
6. 数据缺失时的通用建议
- 标题**景区名称**游览建议
- 实时客流提示
- 常规交通建议
- 游览路线说明
- 附近景区推荐
语言风格要求
- 使用生动形象的语言描述适当添加emoji表情符号增强表现力
- 采用"""记得""推荐"等暖心词汇
- 禁用专业术语
- 使用**加粗**强调关键信息
- 结尾提示"动态信息请以现场为准,祝您旅途愉快!"
数据要求
- 仅使用查询到的数据
- 不虚构未提供的信息
- 数字数据仅展示计算后的在园人数不显示例如进入人数离开人数承载量和承载率等原始数据字段
- 不输出数据更新时间
- 标题字体大小为 1820px加粗显示
- 正文内容字体大小为 1516px
- 行间距为 1.61.8 倍字体大小
- 段落与模块之间上下边距应为1016px
- 不能使用小于 14px 的字体
"""
async def classify(msg: str) -> str: async def classify(msg: str) -> str:
print(f"Starting classification for message: {msg}") print(f"Starting classification for message: {msg}")
@ -113,10 +168,24 @@ async def classify(msg: str) -> str:
raise raise
async def ai_chat_stream(inp: ChatIn, conversation_history: list) -> AsyncGenerator[str, None]: async def ai_chat_stream(inp: ChatIn, conversation_history: list) -> AsyncGenerator[str, None]:
chat_prompt = ( chat_prompt = f"""
f"你是一个河北省保定市的旅游助手,为游客提供保定市文旅相关问答服务,包括但不限于行程规划、" 你是一个专门服务河北省保定市旅游的AI助手主要提供以下精准服务
f"景点推荐等,不提供其他任何非保定文旅相关信息。输出语言为:{inp.language}" 1. 行程规划根据游客的停留天数(1-7)预算范围(经济型/中档/豪华)兴趣偏好(历史文化/自然风光/美食体验)提供定制化行程方案
) 2. 景点推荐详细介绍保定市3A级以上旅游景区(如白洋淀野三坡清西陵等)的开放时间门票价格最佳游览季节和交通方式
3. 特色推荐提供保定驴肉火烧槐茂酱菜等地方特色美食的具体店铺地址和人均消费
4. 实用信息提供保定市区及周边县市的公共交通线路出租车参考价格天气情况等实用旅行信息
**服务要求**
- 使用生动形象的语言描述适当添加emoji表情符号增强表现力
- 采用Markdown语法组织内容合理使用**加粗***斜体*等格式
- 分点说明时使用清晰的列表格式
- 重要信息使用高亮标记
**服务限制**
- 地理范围仅限保定市行政区划内(含下辖县市)的文旅信息
- 语言输出严格使用用户指定的{inp.language}语言回复
- 问题边界对非保定文旅相关问题统一回复"我是您的保定旅行助手,专注于解答有关保定市的旅行问题哦~"
"""
messages = [{"role": "system", "content": chat_prompt}] + conversation_history messages = [{"role": "system", "content": chat_prompt}] + conversation_history
messages.append({"role": "user", "content": inp.message}) messages.append({"role": "user", "content": inp.message})
@ -150,9 +219,12 @@ async def ai_chat_stream(inp: ChatIn, conversation_history: list) -> AsyncGenera
conversation_history.append({"role": "assistant", "content": full_response}) conversation_history.append({"role": "assistant", "content": full_response})
print("AI chat stream finished.") print("AI chat stream finished.")
def get_formatted_prompt(user_language,msg,data):
# 使用format方法替换占位符
return ANSWER_PROMPT.format(language=user_language, msg=msg, data=data)
async def gen_markdown_stream(msg: str, data: str, language: str, conversation_history: list) -> AsyncGenerator[str, None]: async def gen_markdown_stream(msg: str, data: str, language: str, conversation_history: list) -> AsyncGenerator[str, None]:
prompt = f"""你是一位智能旅游助手,结合用户问题:{msg} 和查询内容:{data}"""+ANSWER_PROMPT + f"""输出语言为:{language}""" prompt = get_formatted_prompt(language,msg,data) # 将变量替换到提示词中
messages = conversation_history + [{"role": "user", "content": prompt}] messages = conversation_history + [{"role": "user", "content": prompt}]
@ -235,8 +307,11 @@ async def query_flow(request: Request, spot: str) -> str:
print(f"No data found for spot: {spot}") print(f"No data found for spot: {spot}")
return f"**未找到景区【{spot}】的信息,请检查名称是否正确。\n\n(内容仅供参考)" return f"**未找到景区【{spot}】的信息,请检查名称是否正确。\n\n(内容仅供参考)"
#随机生成剩余车位数量进行测试
car_num = random.randint(0, 100)
jl_num = random.randint(50, 200)
# 修改结果拼接部分,使用新的列名 # 修改结果拼接部分,使用新的列名
result = f"**{spot} 客流**\n\n进入人数: {row[0]}\n离开人数: {row[1]}\n\n景区瞬时承载量:{row[2]},暂无停车场数据(内容仅供参考)" result = f"**{spot} 客流**\n\n进入人数: {row[0]}\n离开人数: {row[1]}\n\n景区瞬时承载量:{row[2]};停车场名称:临时地上停车场 ,距离:{jl_num}米,空余车位:{car_num},总车位:100,收费标准:30分钟内免费,超出后3元/小时。(内容仅供参考)"
# Step 3: 写入 Redis 缓存 # Step 3: 写入 Redis 缓存
print(f"Writing data to Redis cache for key: {cache_key}") print(f"Writing data to Redis cache for key: {cache_key}")

@ -1,6 +0,0 @@
# app/api/v1/upload/__init__.py
from fastapi import APIRouter
from .upload import upload_api_router as upload_router
upload_api_router = APIRouter()
upload_api_router.include_router(upload_router, prefix='/upload')

@ -10,7 +10,7 @@ from .menus import menus_router
from .roles import roles_router from .roles import roles_router
from .users import users_router from .users import users_router
from .quick_question import quick_question_router from .quick_question import quick_question_router
from app.api.upload import upload_api_router from .upload import upload_api_router
v1_router = APIRouter() v1_router = APIRouter()
@ -22,6 +22,6 @@ v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermiss
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermission]) v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermission])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermission]) v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermission])
v1_router.include_router(quick_question_router, prefix="/question", dependencies=[DependPermission]) v1_router.include_router(quick_question_router, prefix="/question", dependencies=[DependPermission])
v1_router.include_router(upload_api_router) v1_router.include_router(upload_api_router, prefix="/upload", dependencies=[DependPermission])

@ -5,4 +5,4 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true VITE_USE_PROXY = true
# base api # base api
VITE_BASE_API = 'http://localhost:8111/api/v1' VITE_BASE_API = '/api/v1'

@ -17,7 +17,7 @@ export const PROXY_CONFIG = {
* @转发路径 http://localhost:9999/api/v1/user * @转发路径 http://localhost:9999/api/v1/user
*/ */
'/api/v1': { '/api/v1': {
target: 'http://127.0.0.1:9999', target: 'http://127.0.0.1:8111', // 修改为与.env.development中一致的端口
changeOrigin: true, changeOrigin: true,
}, },
} }

@ -87,7 +87,6 @@
</template> </template>
<script setup> <script setup>
// 1watch
import { h, onMounted, ref, resolveDirective, withDirectives, onUnmounted, watch } from 'vue' import { h, onMounted, ref, resolveDirective, withDirectives, onUnmounted, watch } from 'vue'
import { import {
NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch NButton, NForm, NFormItem, NInput, NPopconfirm, NTag, NSwitch
@ -104,42 +103,48 @@ import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables' import { useCRUD } from '@/composables'
import api from '@/api' import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue' import TheIcon from '@/components/icon/TheIcon.vue'
import { getToken } from '@/utils/auth/token'
import { request } from '@/utils/http'; // request
defineOptions({ name: '快捷问题管理' }) defineOptions({ name: '快捷问题管理' })
// //
const editorRef = ref(null) const editorRef = ref(null)
const toolbarConfig = ref({}) const toolbarConfig = ref({})
//
const editorConfig = ref({ const editorConfig = ref({
placeholder: '请输入问题内容...', placeholder: '请输入问题内容...',
MENU_CONF: { MENU_CONF: {
uploadImage: { uploadImage: {
server: '/api/upload/image', server: '/api/v1/upload/image',
fieldName: 'file', fieldName: 'file',
maxFileSize: 2 * 1024 * 1024, // 2MB maxFileSize: 2 * 1024 * 1024,
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` 'token': getToken()
}, },
//
onSuccess: (file, res) => {
console.log('上传成功', file, res)
},
//
onFailed: (file, err, res) => { onFailed: (file, err, res) => {
console.error('上传失败', file, err, res) console.error('上传回调失败', file, err, res)
},
// Add upload progress callback for debugging
onProgress: (progress) => {
console.log('Upload progress:', progress)
} }
}, },
uploadVideo: { uploadVideo: {
server: '/api/upload/video', server: '/api/v1/upload/video',
fieldName: 'file', fieldName: 'file',
maxFileSize: 100 * 1024 * 1024, // 100MB maxFileSize: 100 * 1024 * 1024,
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` 'token': getToken()
},
onProgress: (progress) => {
console.log('Upload progress:', progress)
} }
} }
} }
}) })
const handleEditorChange = (editor) => { const handleEditorChange = (editor) => {
modalForm.value.content = editor.getHtml() modalForm.value.content = editor.getHtml()
} }
@ -198,11 +203,10 @@ const {
refresh: () => $table.value?.handleSearch(), refresh: () => $table.value?.handleSearch(),
}) })
// 2使 // handleSave
// handleSave
const originalHandleSave = handleSave const originalHandleSave = handleSave
// 3customHandleSave //
const customHandleSave = async () => { const customHandleSave = async () => {
if (modalFormRef.value) { if (modalFormRef.value) {
try { try {
@ -219,7 +223,7 @@ const customHandleSave = async () => {
} }
} }
// 4handleEdit //
const handleEdit = async (row) => { const handleEdit = async (row) => {
originalHandleEdit(row); originalHandleEdit(row);
try { try {
@ -341,19 +345,17 @@ const columns = [
}, },
}, },
] ]
// watch
//
watch( watch(
() => modalForm.content, () => modalForm.content,
(newValue) => { (newValue) => {
if (modalFormRef.value) { if (modalFormRef.value) {
// newValue
//
setTimeout(() => { setTimeout(() => {
modalFormRef.value.validateField('content') modalFormRef.value.validateField('content')
}, 100) }, 100)
} }
}, },
// immediate
{ immediate: true } { immediate: true }
) )
</script> </script>
@ -363,9 +365,8 @@ watch(
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 4px;
width: 100%; width: 100%;
/* 增加最小高度和高度设置 */
min-height: 300px; min-height: 300px;
height: 50vh; /* 使用视口高度单位,确保编辑器随页面大小调整 */ height: 50vh;
overflow: hidden; overflow: hidden;
} }
</style> </style>

@ -31,11 +31,6 @@ export default defineConfig(({ command, mode }) => {
proxy: VITE_USE_PROXY proxy: VITE_USE_PROXY
? { ? {
[VITE_BASE_API]: PROXY_CONFIG[VITE_BASE_API], [VITE_BASE_API]: PROXY_CONFIG[VITE_BASE_API],
// 添加对/api/upload路径的代理配置
'/api/upload': {
target: 'http://127.0.0.1:8111',
changeOrigin: true,
},
} }
: undefined, : undefined,
}, },

Loading…
Cancel
Save