zc 3 months ago
parent eb54315eb9
commit e8641454fd
  1. 4
      app/api/__init__.py
  2. 3
      app/api/chat/ai/chat_router.py
  3. 180
      app/api/chat/ai/chat_service.py
  4. 2
      app/api/upload/__init__.py
  5. 8
      app/api/upload/upload/__init__.py
  6. 0
      app/api/upload/upload/upload.py
  7. 13
      app/settings/config.py
  8. 17
      web/src/views/system/question/index.vue
  9. 5
      web/vite.config.js

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

@ -67,7 +67,7 @@ async def h5_chat_stream(request: Request, inp: ChatIn):
try:
return StreamingResponse(
content_stream(),
media_type="text/event-stream",
media_type="text/plain",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
@ -78,6 +78,7 @@ async def h5_chat_stream(request: Request, inp: ChatIn):
print(f"Error in StreamingResponse: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
class ClearConversationRequest(BaseModel):
user_id: int

@ -12,11 +12,91 @@ load_dotenv()
client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url=settings.DEEPSEEK_API_URL)
#分类提示词
CATEGORY_PROMPT = """你是一个分类助手,请根据用户的问题判断属于哪一类:
1. 如果用户是问某个保定市景区现在适不适合去或者某个保定市景区现在人多么此类涉及某个景区人数或者客流量的注意只有保定的景区请返回游玩判断
2. 其他均返回保定文旅
只能返回以上两个分类词之一不能多说话不回复其他多余内容"""
EXTRACT_PROMPT = """你是一个中文旅游助手,请从用户的问题中提取出景区名称,仅返回景区名,不要多余文字;如果没有提到具体景区,返回空字符串。"""
#提取景区名称提示词
EXTRACT_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个周边承载率低的景区并说明优势
**温馨提示**如安全提醒天气影响等影响游客游览的内容语言风格亲切如朋友交谈"""记得""推荐"等暖心词汇禁用机械术语**加粗**突出关键建议加入表情符号增加亲和力不展示原始字段数据结尾不输出数据时间仅加提示动态信息请以现场为准祝您旅途愉快
若未找到景区的信息时生成景区通用游览建议使用以下输出结构
首行为标题**景区名称** 游览建议景区名称要显示正确完整
**实时客流**仅显示提示暂时无法获取实时客流数据您仍可参考以下出行建议
**出行与游览建议**交通方式列出几种常规交通方式并推荐大众选择较多的出行方式简单描述优势和注意事项
游览路线描述游览景区常规路线和游览重点突出表现景区特色
游览时间显示常规参观时间旺季需多预留30%不要直接显示百分比需提出具体时长如1小时以上
附近景区推荐推荐1-2个周边景区并说明景区特点
**温馨提示**如安全提醒天气影响等影响游客游览的内容语言风格亲切如朋友交谈"""记得""推荐"等暖心词汇禁用机械术语**加粗**突出关键建议加入表情符号增加亲和力不展示原始字段数据结尾不输出数据时间仅加提示动态信息请以现场为准祝您旅途愉快"""
async def classify(msg: str) -> str:
print(f"Starting classification for message: {msg}")
@ -41,32 +121,70 @@ async def ai_chat_stream(inp: ChatIn, conversation_history: list) -> AsyncGenera
messages.append({"role": "user", "content": inp.message})
print(f"Starting AI chat stream with input: {inp.message}")
full_response = ""
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
full_response = ""
# 使用异步方式处理同步流
import asyncio
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
# 直接输出纯文本内容,无SSE格式
yield delta.content
# 保留缓冲区刷新
import sys
sys.stdout.flush()
# 可选:添加结束标记(如需要)
# yield "[STREAM_END]"
# 短暂让出控制权,避免阻塞
await asyncio.sleep(0)
# 添加结束标记
yield " "
except Exception as e:
print(f"Error in AI chat stream: {e}")
raise
error_msg = f"当前访问人数过多,请稍后重试: {str(e)}"
print(error_msg)
yield error_msg
# 将 AI 的回复添加到对话历史中
conversation_history.append({"role": "assistant", "content": full_response})
if full_response:
conversation_history.append({"role": "assistant", "content": full_response})
print("AI chat stream finished.")
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}"""
messages = conversation_history + [{"role": "user", "content": prompt}]
print(f"Starting markdown stream with message: {msg} and data: {data}")
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
if full_response:
conversation_history.append({"role": "assistant", "content": full_response})
print("Markdown stream finished.")
async def extract_spot(msg: str) -> str:
print(f"Starting spot extraction for message: {msg}")
try:
@ -127,44 +245,4 @@ async def query_flow(request: Request, spot: str) -> str:
except Exception as e:
print(f"[Redis] 写缓存失败: {e}")
return result
async def gen_markdown_stream(msg: str, data: str, language: str, conversation_history: list) -> AsyncGenerator[str, None]:
prompt = f"""结合用户问题:{msg} 和查询内容:{data},生成出行建议,
包括景区实时信息展示景区名称和在园人数=进入人数-离开人数若为负取绝对值超出最大承载量时仅展示在园人数
进入/离开人数和瞬时承载量不展示承载率仅以舒适度等级汉字形式体现不展示百分比
舒适度解读根据承载率范围输出等级并用 HTML 字体颜色标签展示
<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-2个周边承载率低的同类景区并说明优势
温馨提示如安全提醒天气影响整体风格友好专业有条理语言自然不展示原始字段数据结尾不输出数据时间仅加提示结果仅供参考输出语言为{language}"""
messages = conversation_history + [{"role": "user", "content": prompt}]
print(f"Starting markdown stream with message: {msg} and data: {data}")
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
full_response = ""
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
# 删除SSE格式封装,改为纯文本输出
yield delta.content
# 添加缓冲区刷新
import sys
sys.stdout.flush()
# 删除SSE结束标记
# yield "data: [DONE]\n\n"
except Exception as e:
print(f"Error in markdown stream: {e}")
raise
# 将 AI 的回复添加到对话历史中
conversation_history.append({"role": "assistant", "content": full_response})
print("Markdown stream finished.")
return result

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

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .upload import router
upload_api_router = APIRouter()
upload_api_router.include_router(router, tags=["上传接口"])
__all__ = ["upload_api_router"]

@ -51,12 +51,15 @@ class Settings(BaseSettings):
"mysql": {
"engine": "tortoise.backends.mysql",
"credentials": {
"host": "39.105.17.128", # Database host address
"port": 33068, # Database port
"user": "root", # Database username
"password": "Mysql@1303", # Database password
"database": "bd_ai_fastapi", # Database name
"host": "39.105.17.128",
"port": 33068,
"user": "root",
"password": "Mysql@1303",
"database": "bd_ai_fastapi",
},
"pool_size": 20, # 增加连接池大小
"max_overflow": 10, # 允许临时超过连接池大小的连接数
"pool_recycle": 300 # 连接回收时间(秒)
},
# PostgreSQL configuration
# Install with: tortoise-orm[asyncpg]

@ -76,6 +76,7 @@
v-model:value="modalForm.content"
:defaultConfig="editorConfig"
@onCreated="handleEditorCreated"
@onChange="handleEditorChange"
height="400px"
/>
</div>
@ -113,15 +114,23 @@ const editorConfig = ref({
placeholder: '请输入问题内容...',
MENU_CONF: {
uploadImage: {
server: '/api/upload/image',
server: '/api/upload/image',
fieldName: 'file',
maxFileSize: 2 * 1024 * 1024, // 2MB
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
//
onSuccess: (file, res) => {
console.log('上传成功', file, res)
},
//
onFailed: (file, err, res) => {
console.error('上传失败', file, err, res)
}
},
uploadVideo: {
server: '/api/upload/video',
server: '/api/upload/video',
fieldName: 'file',
maxFileSize: 100 * 1024 * 1024, // 100MB
headers: {
@ -131,6 +140,10 @@ const editorConfig = ref({
}
})
const handleEditorChange = (editor) => {
modalForm.value.content = editor.getHtml()
}
//
const editorCreated = ref(false)

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

Loading…
Cancel
Save