222 lines
8.4 KiB
Python
222 lines
8.4 KiB
Python
|
|
"""
|
||
|
|
用户行为指纹引擎 — 从行为日志中学习偏好权重和决策规则
|
||
|
|
"""
|
||
|
|
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()
|