Files
Neo-ZQYY/apps/backend/app/services/note_reclassifier.py

218 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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,
}