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
+91 -1
View File
@@ -115,9 +115,53 @@ async def list_conversations(
for emp_id in employee_ids:
employee_avatar_map[emp_id] = await session_service._get_employee_avatar(emp_id)
# ── BUGFIX: 批量回退查询员工信息 ──
# 为什么需要:conversations 表中 employee_name/department/position 是冗余字段,
# 在会话创建时可能为空(异步创建、企微回调延迟等),导致列表API返回空字符串。
# 当这些字段为空时,从 employees 表批量查询并回填,确保坐席端能看到完整用户信息。
# 何时触发:仅当 conversations 表中的 employee_name 为空字符串时才会去 employees 表查找。
employee_name_map: dict[str, dict] = {}
empty_name_conv_ids = [
conv.employee_id for conv in conversations
if not conv.employee_name and conv.employee_id
]
if empty_name_conv_ids:
try:
from app.models.employee import Employee
stmt = select(Employee).where(Employee.employee_id.in_(empty_name_conv_ids))
result = await db.execute(stmt)
for emp in result.scalars().all():
employee_name_map[emp.employee_id] = {
"name": emp.name or "",
"department": emp.department or "",
"position": emp.position or "",
"level": getattr(emp, "it_level", "") or "",
}
if employee_name_map:
logger.info(
f"从employees表批量回退获取员工信息: "
f"请求={len(empty_name_conv_ids)}, 命中={len(employee_name_map)}"
)
except Exception as e:
logger.warning(f"从employees表批量回退获取员工信息失败: error={e}")
# 转换为响应 Schema,附加 is_mine / assigned_agent_name / can_grab / avatar 字段
items = []
for conv in conversations:
# ── BUGFIX: 应用 employees 表回退信息 ──
# 如果 conv 的 employee_name 为空,用批量查询结果回填
# 这样 ConversationResponse.model_validate 序列化时就能拿到正确的值
emp_fallback = employee_name_map.get(conv.employee_id, {})
if emp_fallback:
if not conv.employee_name and emp_fallback.get("name"):
conv.employee_name = emp_fallback["name"]
if not conv.department and emp_fallback.get("department"):
conv.department = emp_fallback["department"]
if not conv.position and emp_fallback.get("position"):
conv.position = emp_fallback["position"]
if not conv.level and emp_fallback.get("level"):
conv.level = emp_fallback["level"]
conv_data = ConversationResponse.model_validate(conv).model_dump()
# 员工头像(从缓存获取)
conv_data["avatar"] = employee_avatar_map.get(conv.employee_id, "")
@@ -188,7 +232,7 @@ async def get_conversation(
conversation.employee_name = employee.name
conversation.department = employee.department or ""
conversation.position = employee.position or ""
conversation.level = employee.level or ""
conversation.level = getattr(employee, "it_level", "") or ""
logger.info(
f"从employees表回退获取会话详情员工信息: employee_id={conversation.employee_id}, "
f"name={employee.name}"
@@ -275,6 +319,7 @@ async def resolve_conversation(
"""结单。
坐席点击"结单"按钮时调用,将会话状态改为 resolved。
结单完成后异步触发知识建议生成(通道 A 全链路闭环)。
权限控制:只有主责坐席(assigned_agent_id)才能结单。
协作坐席和其他坐席不能结单。
@@ -303,6 +348,51 @@ async def resolve_conversation(
conversation = await session_service.resolve_conversation(conversation_id)
response_data = ConversationResponse.model_validate(conversation).model_dump()
# ── 任务1(P0):会话关闭→异步触发知识建议生成 ──
# 在结单响应返回后,异步调用 Dify 生成知识迭代建议。
# 使用 FastAPI BackgroundTasks 确保不阻塞结单响应。
try:
from fastapi import BackgroundTasks
import asyncio as _asyncio
async def _trigger_knowledge_suggestion():
"""异步生成知识建议的后台任务(独立 db session)。"""
from app.database import _get_session_factory
from app.services.knowledge_iteration_service import KnowledgeIterationService
factory = _get_session_factory()
async with factory() as bg_db:
try:
knowledge_service = KnowledgeIterationService()
suggestion = await knowledge_service.generate_knowledge_suggestion(
db=bg_db,
source_type="conversation",
source_data=[str(conversation_id)],
reason=f"会话'{conversation_id}'已结单,自动生成知识迭代建议",
)
if suggestion:
bg_db.add(suggestion)
await bg_db.commit()
logger.info(
f"会话关闭→知识建议已生成: conv_id={conversation_id}, "
f"suggestion_id={suggestion.id}, type={suggestion.suggestion_type}"
)
else:
logger.info(
f"会话关闭→无知识建议生成(Dify不可用或无需建议): "
f"conv_id={conversation_id}"
)
except Exception as e:
logger.error(f"会话关闭→知识建议生成失败: conv_id={conversation_id}, error={e}")
# 创建后台任务(不阻塞结单响应)
_asyncio.ensure_future(_trigger_knowledge_suggestion())
logger.info(f"会话结单完成,已触发异步知识建议生成: conv_id={conversation_id}")
except Exception as e:
# 知识建议生成失败不影响结单主流程
logger.warning(f"触发异步知识建议生成失败(不影响结单): {e}")
return success_response(data=response_data)