"""任务书生成器:基于规则生成 AI 续聘建议 + 性能摘要 + 材料清单。 本地规则版(v1.1.0):不调用 LLM。 后续可替换为: - jiangchang_skill_core.llm_manager 调用 LLM - 或在 generate_task_for_contract 里加 LLM 决策分支 """ from __future__ import annotations import sqlite3 from db import repository as repo from util.constants import ( DEFAULT_MATERIALS, MORAL_FAIL_THRESHOLD, PERFORMANCE_FAIL_THRESHOLD, PERFORMANCE_RENEW_MIN, ) from util.logging_setup import get_skill_logger logger = get_skill_logger() def suggest(perf_records: list[sqlite3.Row]) -> tuple[str, str]: """基于近 3 年业绩规则生成续聘建议。 返回 (suggestion, reason)。 规则(顺序判断): 1. 师德评分 < MORAL_FAIL_THRESHOLD(60)→ non_renew 2. 任意一年考核 < PERFORMANCE_FAIL_THRESHOLD → observe 3. 全部 >= PERFORMANCE_RENEW_MIN 且逐年上升 → renew 4. 其他 → observe """ if not perf_records: return ("observe", "近 3 年业绩数据缺失,需人工补录") # 取最近一条作为师德评分参考(最新一年) moral = perf_records[-1]["moral_teaching_score"] if moral < MORAL_FAIL_THRESHOLD: return ("non_renew", f"师德师风考评 {moral:.0f} 分,未达标") scores = [p["performance_score"] for p in perf_records] fail_years = sum(1 for s in scores if s < PERFORMANCE_FAIL_THRESHOLD) if fail_years >= 1: return ("observe", f"近 3 年考核有 {fail_years} 次不合格(< {PERFORMANCE_FAIL_THRESHOLD} 分),建议复核") # 全部 >= 80 且逐年上升(升序排 → 严格递增) all_high = all(s >= PERFORMANCE_RENEW_MIN for s in scores) upward = all(scores[i] <= scores[i + 1] for i in range(len(scores) - 1)) if all_high and upward: return ("renew", f"近 3 年考核均 ≥ {PERFORMANCE_RENEW_MIN} 分且逐年上升") return ("observe", "进入常规续聘评审流程") def summarize(perf_records: list[sqlite3.Row]) -> str: """生成近 3 年业绩摘要文本(HTML 任务书里 pre-wrap 显示)。""" if not perf_records: return "(暂无业绩数据,需补录)" lines = [] for p in perf_records: lines.append( f"{p['year']} 年度:综合考核 {p['performance_score']:.0f} 分" f"(教学 {p['teaching_score']:.0f} / 科研 {p['research_score']:.0f})" f",师德 {p['moral_teaching_score']:.0f} 分" ) return "\n".join(lines) def generate_task_for_contract( conn: sqlite3.Connection, contract: sqlite3.Row, alert_id: str, task_id: str, ) -> dict | None: """为单份合同生成 renewal_task。 返回写入库的 task 摘要 dict(不入库时返回 None)。 """ perf = repo.list_performance_by_contract(conn, contract["contract_id"]) suggestion, reason = suggest(perf) summary = summarize(perf) repo.insert_task( conn, task_id=task_id, contract_id=contract["contract_id"], alert_id=alert_id, suggestion=suggestion, reason=reason, summary=summary, checklist=list(DEFAULT_MATERIALS), ) logger.info( "task_generator: %s contract=%s → %s (%s)", task_id, contract["contract_id"], suggestion, reason, ) return { "task_id": task_id, "contract_id": contract["contract_id"], "ai_suggestion": suggestion, "suggestion_reason": reason, } def regenerate_task_for_contract_id( conn: sqlite3.Connection, contract_id: str, task_id: str | None = None ) -> dict: """单独 generate-renewal-task CLI 入口:基于已有合同 + 最新 alert 生成任务。 如果没有 alert,自动创建 yellow 级占位 alert(合同已临近)。 """ contract = repo.get_contract(conn, contract_id) if contract is None: raise ValueError(f"contract_id={contract_id!r} 不存在") # 取最新 open alert(如果有) alert = conn.execute( """ SELECT * FROM alerts WHERE contract_id = ? AND status = 'open' ORDER BY generated_at DESC LIMIT 1 """, (contract_id,), ).fetchone() if alert is None: # 自动建一个 yellow 级 placeholder alert from util.date_util import days_to, parse_iso_date days = days_to(parse_iso_date(contract["contract_end"])) from util.constants import DEFAULT_THRESHOLDS from util.date_util import classify_alert level = classify_alert(days, DEFAULT_THRESHOLDS) or "yellow" import uuid alert_id = f"A-{contract_id}-manual-{uuid.uuid4().hex[:6]}" repo.insert_alert(conn, alert_id, contract_id, level, days, scan_id="manual") alert = {"alert_id": alert_id, "alert_level": level, "days_to_expire": days} task_id = task_id or f"T-{contract_id}-manual" return generate_task_for_contract(conn, contract, alert["alert_id"], task_id)