From e8641454fde18bdc05b0f5a6bd84746b89091a71 Mon Sep 17 00:00:00 2001 From: zc Date: Fri, 1 Aug 2025 09:02:50 +0800 Subject: [PATCH] -- --- app/api/__init__.py | 4 +- app/api/chat/ai/chat_router.py | 3 +- app/api/chat/ai/chat_service.py | 180 +++++++++++++++++------- app/api/upload/__init__.py | 2 +- app/api/upload/upload/__init__.py | 8 ++ app/api/upload/{ => upload}/upload.py | 0 app/settings/config.py | 13 +- web/src/views/system/question/index.vue | 17 ++- web/vite.config.js | 5 + 9 files changed, 170 insertions(+), 62 deletions(-) create mode 100644 app/api/upload/upload/__init__.py rename app/api/upload/{ => upload}/upload.py (100%) diff --git a/app/api/__init__.py b/app/api/__init__.py index d74cb62..30df3db 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -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"] diff --git a/app/api/chat/ai/chat_router.py b/app/api/chat/ai/chat_router.py index 5f54541..fe8bc6c 100644 --- a/app/api/chat/ai/chat_router.py +++ b/app/api/chat/ai/chat_router.py @@ -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 diff --git a/app/api/chat/ai/chat_service.py b/app/api/chat/ai/chat_service.py index a81f78f..48ee766 100644 --- a/app/api/chat/ai/chat_service.py +++ b/app/api/chat/ai/chat_service.py @@ -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 分钟,通道拥堵,拍照困难,可能限流),如 舒适 较舒适 一般 较拥挤 拥挤,不要加括号或冗余说明,仅展示彩色词语), +**周边停车指南**(展示停车场名称,距景区距离,步行所需时间,当前余位,收费标准;每项数据为一行,收费标准需提示“具体以现场公示为准”), + **出行与游览建议**(是否前往:强烈推荐/推荐/谨慎考虑/暂缓前往,用生活化建议替代机械的等级描述,如:强烈推荐可表述为现在正是游览的好时机!馆内人少舒适,无需排队等候,可以随到随游,享受宁静的观展氛围;当较拥挤或拥挤时显示错峰建议:如当日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 字体颜色标签展示, - 如 舒适 较舒适 一般 较拥挤 拥挤,不要加括号或冗余说明,仅展示彩色词语), - 【核心出行建议】(是否前往:强烈推荐/推荐/谨慎考虑/暂缓前往/强烈不建议;错峰建议:如当日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.") \ No newline at end of file + return result \ No newline at end of file diff --git a/app/api/upload/__init__.py b/app/api/upload/__init__.py index 4c79533..ffe587f 100644 --- a/app/api/upload/__init__.py +++ b/app/api/upload/__init__.py @@ -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') \ No newline at end of file diff --git a/app/api/upload/upload/__init__.py b/app/api/upload/upload/__init__.py new file mode 100644 index 0000000..02ae86b --- /dev/null +++ b/app/api/upload/upload/__init__.py @@ -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"] \ No newline at end of file diff --git a/app/api/upload/upload.py b/app/api/upload/upload/upload.py similarity index 100% rename from app/api/upload/upload.py rename to app/api/upload/upload/upload.py diff --git a/app/settings/config.py b/app/settings/config.py index c539ec7..bf00325 100644 --- a/app/settings/config.py +++ b/app/settings/config.py @@ -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] diff --git a/web/src/views/system/question/index.vue b/web/src/views/system/question/index.vue index d7fa944..3479dd7 100644 --- a/web/src/views/system/question/index.vue +++ b/web/src/views/system/question/index.vue @@ -76,6 +76,7 @@ v-model:value="modalForm.content" :defaultConfig="editorConfig" @onCreated="handleEditorCreated" + @onChange="handleEditorChange" height="400px" /> @@ -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) diff --git a/web/vite.config.js b/web/vite.config.js index 9828e40..8133af2 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -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, },