微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
217
apps/backend/app/services/note_reclassifier.py
Normal file
217
apps/backend/app/services/note_reclassifier.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
备注回溯重分类器(Note Reclassifier)
|
||||
|
||||
召回完成后,回溯检查是否有普通备注需重分类为回访备注。
|
||||
查找 service_time 之后的第一条 normal 备注 → 更新为 follow_up →
|
||||
触发 AI 应用 6 接口(占位)→ 根据 ai_score 生成 follow_up_visit 任务。
|
||||
|
||||
由 trigger_jobs 中的 note_reclassify_backfill 配置驱动(event: recall_completed)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_connection():
|
||||
"""延迟导入 get_connection,避免模块级导入失败。"""
|
||||
from app.database import get_connection
|
||||
|
||||
return get_connection()
|
||||
|
||||
|
||||
def _insert_history(
|
||||
cur,
|
||||
task_id: int,
|
||||
action: str,
|
||||
old_status: str | None = None,
|
||||
new_status: str | None = None,
|
||||
old_task_type: str | None = None,
|
||||
new_task_type: str | None = None,
|
||||
detail: dict | None = None,
|
||||
) -> None:
|
||||
"""在 coach_task_history 中记录变更。"""
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_task_history
|
||||
(task_id, action, old_status, new_status,
|
||||
old_task_type, new_task_type, detail)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
task_id,
|
||||
action,
|
||||
old_status,
|
||||
new_status,
|
||||
old_task_type,
|
||||
new_task_type,
|
||||
json.dumps(detail) if detail else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def ai_analyze_note(note_id: int) -> int | None:
|
||||
"""
|
||||
AI 应用 6 备注分析接口(占位)。
|
||||
|
||||
P5 AI 集成层实现后替换此占位函数。
|
||||
当前返回 None 表示 AI 未就绪,跳过评分逻辑。
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def run(payload: dict | None = None) -> dict:
|
||||
"""
|
||||
备注回溯主流程。
|
||||
|
||||
payload 包含: {site_id, assistant_id, member_id, service_time}
|
||||
|
||||
1. 查找 biz.notes 中该 (site_id, target_type='member', target_id=member_id)
|
||||
在 service_time 之后提交的第一条 type='normal' 的备注
|
||||
2. 将该备注 type 从 'normal' 更新为 'follow_up'
|
||||
3. 触发 AI 应用 6 接口(P5 实现,本 SPEC 仅定义触发接口):
|
||||
- 调用 ai_analyze_note(note_id) → 返回 ai_score
|
||||
4. 若 ai_score >= 6:
|
||||
- 生成 follow_up_visit 任务,status='completed'(回溯完成)
|
||||
5. 若 ai_score < 6:
|
||||
- 生成 follow_up_visit 任务,status='active'(需助教重新备注)
|
||||
|
||||
返回: {"reclassified_count": int, "tasks_created": int}
|
||||
"""
|
||||
if not payload:
|
||||
return {"reclassified_count": 0, "tasks_created": 0}
|
||||
|
||||
site_id = payload.get("site_id")
|
||||
assistant_id = payload.get("assistant_id")
|
||||
member_id = payload.get("member_id")
|
||||
service_time = payload.get("service_time")
|
||||
|
||||
if not all([site_id, assistant_id, member_id, service_time]):
|
||||
logger.warning("备注回溯缺少必要参数: %s", payload)
|
||||
return {"reclassified_count": 0, "tasks_created": 0}
|
||||
|
||||
reclassified_count = 0
|
||||
tasks_created = 0
|
||||
|
||||
conn = _get_connection()
|
||||
try:
|
||||
# ── 1. 查找 service_time 之后的第一条 normal 备注 ──
|
||||
note_id = None
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM biz.notes
|
||||
WHERE site_id = %s
|
||||
AND target_type = 'member'
|
||||
AND target_id = %s
|
||||
AND type = 'normal'
|
||||
AND created_at > %s
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
""",
|
||||
(site_id, member_id, service_time),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
note_id = row[0]
|
||||
conn.commit()
|
||||
|
||||
if note_id is None:
|
||||
logger.info(
|
||||
"未找到符合条件的 normal 备注: site_id=%s, member_id=%s",
|
||||
site_id, member_id,
|
||||
)
|
||||
return {"reclassified_count": 0, "tasks_created": 0}
|
||||
|
||||
# ── 2. 将备注 type 从 'normal' 更新为 'follow_up' ──
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("BEGIN")
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.notes
|
||||
SET type = 'follow_up', updated_at = NOW()
|
||||
WHERE id = %s AND type = 'normal'
|
||||
""",
|
||||
(note_id,),
|
||||
)
|
||||
conn.commit()
|
||||
reclassified_count = 1
|
||||
|
||||
# ── 3. 触发 AI 应用 6 接口(占位,当前返回 None) ──
|
||||
ai_score = ai_analyze_note(note_id)
|
||||
|
||||
# ── 4/5. 根据 ai_score 生成 follow_up_visit 任务 ──
|
||||
if ai_score is not None:
|
||||
if ai_score >= 6:
|
||||
# 回溯完成:生成 completed 任务
|
||||
task_status = "completed"
|
||||
else:
|
||||
# 需助教重新备注:生成 active 任务
|
||||
task_status = "active"
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("BEGIN")
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_tasks
|
||||
(site_id, assistant_id, member_id, task_type,
|
||||
status, completed_at, completed_task_type)
|
||||
VALUES (
|
||||
%s, %s, %s, 'follow_up_visit',
|
||||
%s,
|
||||
CASE WHEN %s = 'completed' THEN NOW() ELSE NULL END,
|
||||
CASE WHEN %s = 'completed' THEN 'follow_up_visit' ELSE NULL END
|
||||
)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
site_id, assistant_id, member_id,
|
||||
task_status, task_status, task_status,
|
||||
),
|
||||
)
|
||||
new_task_row = cur.fetchone()
|
||||
new_task_id = new_task_row[0]
|
||||
|
||||
# 记录任务创建历史
|
||||
_insert_history(
|
||||
cur,
|
||||
new_task_id,
|
||||
action="created_by_reclassify",
|
||||
old_status=None,
|
||||
new_status=task_status,
|
||||
old_task_type=None,
|
||||
new_task_type="follow_up_visit",
|
||||
detail={
|
||||
"note_id": note_id,
|
||||
"ai_score": ai_score,
|
||||
"source": "note_reclassifier",
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
tasks_created = 1
|
||||
else:
|
||||
# AI 未就绪,跳过任务创建
|
||||
logger.info(
|
||||
"AI 接口未就绪,跳过任务创建: note_id=%s", note_id
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"备注回溯失败: site_id=%s, member_id=%s",
|
||||
site_id, member_id,
|
||||
)
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
logger.info(
|
||||
"备注回溯完成: reclassified_count=%d, tasks_created=%d",
|
||||
reclassified_count, tasks_created,
|
||||
)
|
||||
return {
|
||||
"reclassified_count": reclassified_count,
|
||||
"tasks_created": tasks_created,
|
||||
}
|
||||
Reference in New Issue
Block a user