# -*- 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