feat(backend): knowledge iteration + vision + neo4j + response contract source

dependencies.py 拆分为 dependencies/ 包; 新增 vision/ragflow_ingestion/neo4j 客户端与 h5_ai_task; alembic 045 图置信度迁移; 响应契约统一收尾。
This commit is contained in:
Simon
2026-07-09 11:47:16 +08:00
parent f5374fce9b
commit ead5f83bee
39 changed files with 5276 additions and 545 deletions
+27 -75
View File
@@ -45,7 +45,7 @@ limiter = Limiter(key_func=get_remote_address)
from app.config import settings
from app.database import get_db
from app.utils.env_gating import is_production
from app.dependencies import dep_redis, dep_wecom_service, dep_ai_handler
from app.dependencies import dep_redis, dep_wecom_service
from app.models.approval_link import ApprovalLink
from app.models.conversation import Conversation
from app.models.message import Message
@@ -58,7 +58,9 @@ from app.schemas.h5 import (
)
from app.schemas.conversation import ConversationResponse, JoinConversationRequest
from app.schemas.message import MessageResponse
from app.services.ai_handler import AIHandler
import asyncio
from app.tasks.h5_ai_task import process_h5_ai_reply
from app.services.funny_phrase_service import FunnyPhraseService
from app.services.ws_manager import manager as ws_manager
from app.services.wecom_service import WecomService
@@ -822,7 +824,6 @@ async def h5_send_message(
body: dict,
employee_id: str = Depends(_get_current_employee),
db: AsyncSession = Depends(get_db),
ai_handler: AIHandler = Depends(dep_ai_handler),
):
"""H5 用户发送消息(含 AI 回复与计数)。
@@ -893,47 +894,9 @@ async def h5_send_message(
db.add(conversation)
await db.flush()
# 3. 调用 AIHandler 统一处理(打招呼检测 → 呼叫人工拦截 → AI 调用
ai_result = await ai_handler.handle_message(
content=content,
dify_conversation_id=conversation.dify_conversation_id,
user_id=employee_id,
)
# 4. 根据 AIHandler 返回结果更新会话状态
# 更新 Dify 会话ID(多轮对话上下文)
if ai_result.dify_conversation_id:
conversation.dify_conversation_id = ai_result.dify_conversation_id
# 更新 AI 实质性回复计数(仅 AI 命中时 +1)
if ai_result.should_count:
conversation.ai_substantive_reply_count += 1
# 更新会话状态(未命中转人工时改为 queued)
if ai_result.should_transfer:
conversation.status = "queued"
db.add(conversation)
# 5. 创建 AI 回复消息
ai_message = Message(
conversation_id=conversation.id,
sender_type="ai",
sender_id="ai_bot",
sender_name="AI智能助手",
content=ai_result.content,
msg_type="text",
is_read=True,
)
db.add(ai_message)
await db.flush()
# 6. WebSocket 广播:通知坐席端有新消息
# 做什么:向所有在线坐席广播 new_message 事件,携带用户消息和 AI 回复
# 为什么:坐席端需要实时看到员工的新消息和 AI 回复,
# 仅靠3秒轮询会有延迟,WS 推送更实时
# 3. 广播用户消息给坐席端(员工端靠乐观更新已显示自己消息
# 为什么:坐席端需实时看到员工新消息,仅依赖 3 秒轮询会有延迟
try:
# 广播用户消息
await ws_manager.broadcast({
"type": "new_message",
"data": {
@@ -948,41 +911,30 @@ async def h5_send_message(
"tags": conversation.tags,
},
})
# 广播 AI 回复
await ws_manager.broadcast({
"type": "new_message",
"data": {
"conversation_id": str(conversation.id),
"message_id": str(ai_message.id),
"sender_type": "ai",
"sender_id": "ai_bot",
"sender_name": "AI智能助手",
"content": ai_result.content,
"msg_type": "text",
},
})
# 如果会话状态变更(如新会话创建或转人工),也广播状态变更
await ws_manager.broadcast({
"type": "conversation_updated",
"data": {
"conversation_id": str(conversation.id),
"status": conversation.status,
"assigned_agent_id": str(conversation.assigned_agent_id) if conversation.assigned_agent_id else None,
},
})
except Exception as ws_err:
# WS 广播失败不阻塞消息存储,只记录 warning
logger.warning(f"WS 广播消息失败(消息已存储): {ws_err}")
logger.warning(f"WS 广播用户消息失败(消息已存储): {ws_err}")
# 7. 返回用户消息 + AI 回复
# 4. 启动后台 AI 任务(异步,不阻塞 HTTP 返回)
# 为什么:AI 推理(Dify)慢(3~15s),放后台经 WS 流式推回,
# 发送接口瞬时返回,前端不再卡"发送中"
# 约束:后台任务使用独立 DB session,且需单 worker(见 h5_ai_task.py
asyncio.create_task(
process_h5_ai_reply(
conversation_id=str(conversation.id),
employee_id=employee_id,
content=content,
dify_conversation_id=conversation.dify_conversation_id,
)
)
# 5. 立即返回用户消息(AI 回复经 WS 异步推送,不在此同步返回)
user_msg_data = MessageResponse.model_validate(message).model_dump()
ai_msg_data = MessageResponse.model_validate(ai_message).model_dump()
return success_response(
data={
"user_message": user_msg_data,
"ai_reply": ai_msg_data,
"is_guidance": ai_result.is_guidance,
"ai_reply": None,
"is_guidance": False,
"ai_reply_count": conversation.ai_substantive_reply_count,
"can_call_agent": conversation.ai_substantive_reply_count >= 3,
"conversation_status": conversation.status,
@@ -1173,14 +1125,14 @@ async def shake(
# 无活跃会话 → 拒绝,必须先与 AI 互动(前端按钮此时不应出现,这是后端兜底)
raise AppException(
1003,
"请先描述您的问题,AI助手需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
"请先描述您的问题,Duckula(达寇拉)需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
)
# 前置校验:必须满足 AI 实质性回复 >= 3 次才能呼叫坐席
if conversation.ai_substantive_reply_count < 3:
raise AppException(
1003,
"请先描述您的问题,AI助手需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
"请先描述您的问题,Duckula(达寇拉)需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
)
# 更新员工姓名
@@ -1327,14 +1279,14 @@ async def call_agent(
if not conversation:
raise AppException(
code=1003,
message="请先描述您的问题,AI助手需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
message="请先描述您的问题,Duckula(达寇拉)需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
)
# 2. 前置校验:必须满足 AI 实质性回复 >= 3 次
if conversation.ai_substantive_reply_count < 3:
raise AppException(
code=1003,
message="请先描述您的问题,AI助手需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
message="请先描述您的问题,Duckula(达寇拉)需要先帮您分析。至少互动3轮后才能呼叫人工坐席哦~"
)
# 更新员工姓名