""" 用户行为指纹引擎 — 从行为日志中学习偏好权重和决策规则 """ from __future__ import annotations import logging from typing import Any, Dict, List, Optional from collections import Counter from sqlalchemy.orm import Session from sqlalchemy import func from app.core.database import SessionLocal from app.models.user_behavior import UserBehaviorLog, BehaviorCategory from app.models.user_fingerprint import UserFingerprint logger = logging.getLogger(__name__) DEFAULT_WEIGHTS = { "code_review": {"security": 0.3, "performance": 0.25, "readability": 0.25, "style": 0.2}, "document": {"structure": 0.3, "clarity": 0.3, "completeness": 0.25, "style": 0.15}, "decision": {"data_driven": 0.4, "risk_averse": 0.3, "speed": 0.3}, "email": {"formality": 0.3, "conciseness": 0.35, "responsiveness": 0.35}, } class FingerprintEngine: """从行为日志学习用户行为指纹""" def compute_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]: """计算用户行为指纹。""" db: Optional[Session] = None try: db = SessionLocal() total = db.query(func.count(UserBehaviorLog.id)).filter( UserBehaviorLog.user_id == user_id ).scalar() or 0 if total < 10: logger.info("用户 %s 行为数据不足 (%d 条)", user_id, total) return None by_category = {} for cat in BehaviorCategory: count = db.query(func.count(UserBehaviorLog.id)).filter( UserBehaviorLog.user_id == user_id, UserBehaviorLog.category == cat.value, ).scalar() or 0 by_category[cat.value] = count preference_weights = self._extract_preferences(db, user_id) decision_rules = self._extract_rules(db, user_id) behaviors = ( db.query(UserBehaviorLog) .filter(UserBehaviorLog.user_id == user_id) .order_by(UserBehaviorLog.created_at.desc()) .limit(100) .all() ) durations = [] for b in behaviors: if b.result and isinstance(b.result, dict): d = b.result.get("duration_ms") if d: durations.append(d) avg_response = int(sum(durations) / len(durations)) if durations else None return { "user_id": user_id, "preference_weights": preference_weights, "decision_rules": decision_rules, "total_behaviors": total, "behaviors_by_category": by_category, "avg_response_time_ms": avg_response, } except Exception as e: logger.error("指纹计算失败: %s", e) return None finally: if db: try: db.close() except Exception: pass def _extract_preferences(self, db: Session, user_id: str) -> Dict[str, Any]: weights = dict(DEFAULT_WEIGHTS) recent = ( db.query(UserBehaviorLog) .filter(UserBehaviorLog.user_id == user_id) .order_by(UserBehaviorLog.created_at.desc()) .limit(200) .all() ) for b in recent: if b.result and isinstance(b.result, dict): priority = b.result.get("priority") if priority and isinstance(priority, dict): cat = b.category if cat in weights: for k, v in priority.items(): if k in weights[cat]: weights[cat][k] = weights[cat][k] * 0.9 + v * 0.1 return weights def _extract_rules(self, db: Session, user_id: str) -> List[Dict[str, Any]]: rules = [] recent = ( db.query(UserBehaviorLog) .filter(UserBehaviorLog.user_id == user_id) .order_by(UserBehaviorLog.created_at.desc()) .limit(300) .all() ) action_counts = Counter(b.action for b in recent) for action, count in action_counts.most_common(20): if count >= 5: actions_of_type = [b for b in recent if b.action == action] status_codes = Counter() for b in actions_of_type: if b.result and isinstance(b.result, dict): sc = b.result.get("status_code") if sc: status_codes[sc] += 1 if status_codes and status_codes.most_common(1)[0][1] / count > 0.8: rules.append({ "action": action, "expected_outcome": status_codes.most_common(1)[0][0], "confidence": round(status_codes.most_common(1)[0][1] / count, 2), "sample_count": count, }) return rules[:50] def save_or_update(self, user_id: str, fingerprint: Dict[str, Any]) -> Optional[str]: db: Optional[Session] = None try: db = SessionLocal() existing = db.query(UserFingerprint).filter( UserFingerprint.user_id == user_id ).first() if existing: existing.preference_weights = fingerprint["preference_weights"] existing.decision_rules = fingerprint["decision_rules"] existing.total_behaviors = fingerprint["total_behaviors"] existing.behaviors_by_category = fingerprint["behaviors_by_category"] existing.avg_response_time_ms = fingerprint.get("avg_response_time_ms") existing.model_version = str(float(existing.model_version or "1.0") + 0.1) existing.last_trained_at = func.now() fid = str(existing.id) else: entry = UserFingerprint( user_id=user_id, preference_weights=fingerprint["preference_weights"], decision_rules=fingerprint["decision_rules"], total_behaviors=fingerprint["total_behaviors"], behaviors_by_category=fingerprint["behaviors_by_category"], avg_response_time_ms=fingerprint.get("avg_response_time_ms"), ) db.add(entry) db.flush() fid = str(entry.id) db.commit() return fid except Exception as e: logger.error("保存指纹失败: %s", e) if db: try: db.rollback() except Exception: pass return None finally: if db: try: db.close() except Exception: pass def train(self, user_id: str) -> Optional[Dict[str, Any]]: fingerprint = self.compute_fingerprint(user_id) if fingerprint: self.save_or_update(user_id, fingerprint) logger.info("用户 %s 指纹训练完成: %d 条行为", user_id, fingerprint["total_behaviors"]) return fingerprint def get_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]: db: Optional[Session] = None try: db = SessionLocal() fp = db.query(UserFingerprint).filter( UserFingerprint.user_id == user_id ).first() if not fp: return None return { "user_id": fp.user_id, "preference_weights": fp.preference_weights, "decision_rules": fp.decision_rules, "total_behaviors": fp.total_behaviors, "behaviors_by_category": fp.behaviors_by_category, "avg_response_time_ms": fp.avg_response_time_ms, "model_version": fp.model_version, "last_trained_at": fp.last_trained_at.isoformat() if fp.last_trained_at else None, } except Exception as e: logger.error("获取指纹失败: %s", e) return None finally: if db: try: db.close() except Exception: pass fingerprint_engine = FingerprintEngine()