Files
crm_project/backend/app/services/ai_workflow.py
T
hankin 423baff73b v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成
- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
2026-03-16 07:31:37 +00:00

146 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
"""
AI 工作流服务 (Dify BaaS 版本)
事件驱动的异步 AI 任务:从客户沟通日志中自动提取标签和生成跟进待办。
架构要点:
- 此函数由 FastAPI BackgroundTasks 调用,运行在独立的后台线程中
- 必须使用独立的 DB Session 生命周期,严禁与主请求共享 Session
- AI 调用通过 Dify 平台 API(非直连大模型)
- AI 解析失败时静默降级(记录日志),绝不抛出异常导致主线程崩溃
"""
import json
import logging
import uuid
from app.core.dify_client import dify_client
from app.core.database import AsyncSessionLocal
from app.models.crm_business import CustomerTag, FollowUpToDo
from app.core.config import settings
logger = logging.getLogger("ai_workflow")
async def process_log_with_ai(
log_id: uuid.UUID,
content: str,
customer_id: uuid.UUID,
) -> None:
"""
后台 AI 任务:分析沟通日志,提取标签和待办。
*** 关键约束 ***
此函数在 BackgroundTasks 中执行,必须:
1. 使用独立的 AsyncSession(不与主请求共享)
2. 内部捕获所有异常,不向外抛出
3. AI 返回格式异常时静默降级
"""
logger.info("开始 AI 处理: log_id=%s, customer_id=%s", log_id, customer_id)
if not settings.DIFY_LOG_APP_API_KEY:
logger.error("DIFY_LOG_APP_API_KEY 未配置,跳过 AI 处理")
return
# ---- 独立的 DB Session 生命周期 ----
async with AsyncSessionLocal() as db:
try:
# Step 1: 调用 Dify 日志分析 Workflow App
# *** inputs 的键名必须与 Dify 后台配置的变量名对齐 ***
# 在 Dify 后台的 Workflow 编排中,需定义输入变量 "log_content"
workflow_outputs = await dify_client.call_workflow(
api_key=settings.DIFY_LOG_APP_API_KEY,
inputs={"log_content": content},
)
if not workflow_outputs:
logger.warning("Dify 返回空响应,跳过入库 (log_id=%s)", log_id)
return
# Workflow 返回的是 dict,序列化为 JSON 字符串以兼容现有解析管线
if isinstance(workflow_outputs, dict):
raw_response = json.dumps(workflow_outputs, ensure_ascii=False)
else:
raw_response = str(workflow_outputs)
logger.debug("Dify Workflow 原始返回: %s", raw_response[:500])
# Step 2: 解析 JSON 响应 (容错)
json_str = _extract_json(raw_response)
result = json.loads(json_str)
tags: list[str] = result.get("tags", [])
next_task: str = result.get("next_task", "")
# Step 3: 写入标签 (最多 3 个)
if tags:
for tag_name in tags[:3]:
tag_name = tag_name.strip()
if not tag_name:
continue
tag = CustomerTag(
customer_id=customer_id,
tag_name=tag_name,
)
db.add(tag)
logger.info("写入 %d 个标签: %s", len(tags[:3]), tags[:3])
# Step 4: 写入跟进待办
if next_task and next_task.strip():
todo = FollowUpToDo(
customer_id=customer_id,
task_desc=next_task.strip(),
status="pending",
)
db.add(todo)
logger.info("写入待办: %s", next_task.strip()[:100])
await db.commit()
logger.info("AI 处理完成: log_id=%s", log_id)
except json.JSONDecodeError as e:
logger.error(
"Dify 返回 JSON 解析失败 (log_id=%s): %s | 原始响应: %s",
log_id, e, raw_response[:300],
)
await db.rollback()
except Exception as e:
logger.error(
"AI 后台任务异常 (log_id=%s): %s",
log_id, e, exc_info=True,
)
await db.rollback()
def _extract_json(text: str) -> str:
"""
从 Dify/LLM 响应中提取 JSON 字符串。
处理常见的"包裹"行为:
- 直接返回 JSON
- 用 ```json ... ``` 包裹
- 在 JSON 前后加解释文字
"""
text = text.strip()
# 尝试提取 ```json ... ``` 代码块
if "```json" in text:
start = text.index("```json") + len("```json")
end = text.index("```", start)
return text[start:end].strip()
if "```" in text:
start = text.index("```") + len("```")
end = text.index("```", start)
return text[start:end].strip()
# 尝试找到第一个 { 和最后一个 }
first_brace = text.find("{")
last_brace = text.rfind("}")
if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
return text[first_brace:last_brace + 1]
# 原样返回,让 json.loads 报错触发容错
return text