PMFlow/backend/app/dispatcher.py

181 lines
5.8 KiB
Python

"""Neural Sync - Role Dispatcher。"""
from __future__ import annotations
import logging
import re
from typing import Any, Optional
from pydantic import BaseModel, Field
from .dispatcher_rules import ROLE_RULES, TECH_KEYWORDS, Role, RoutingRule
from .utils.timezone import now_cn_aware
logger = logging.getLogger(__name__)
class RoleRouter:
"""角色路由引擎。"""
ROLE_RULES: list[RoutingRule] = ROLE_RULES
TECH_KEYWORDS = TECH_KEYWORDS
def __init__(self):
self._compiled_patterns: dict[Role, list[re.Pattern]] = {}
for rule in self.ROLE_RULES:
patterns = [re.compile(pattern, re.IGNORECASE) for pattern in rule.patterns]
self._compiled_patterns[rule.role] = patterns
def route(
self,
signal_content: str,
analysis: Optional[dict[str, Any]] = None,
source: Optional[str] = None,
) -> tuple[Role, float, dict[str, Any]]:
analysis = analysis or {}
details = {
"matched_rules": [],
"scores": {},
"reasons": [],
}
if self._should_escalate_to_boss(analysis):
details["reasons"].append("触发老板决策阈值")
details["matched_rules"].append("threshold_check")
return Role.BOSS, 1.0, details
scores: dict[Role, float] = {rule.role: 0.0 for rule in self.ROLE_RULES}
content_lower = signal_content.lower()
for rule in self.ROLE_RULES:
keyword_score = sum(1 for kw in rule.keywords if kw.lower() in content_lower)
if keyword_score > 0:
scores[rule.role] += keyword_score * 0.3
details["scores"][f"{rule.role}_keywords"] = keyword_score
for rule in self.ROLE_RULES:
if rule.role in self._compiled_patterns:
pattern_score = sum(
1
for pattern in self._compiled_patterns[rule.role]
if pattern.search(signal_content)
)
if pattern_score > 0:
scores[rule.role] += pattern_score * 0.5
details["scores"][f"{rule.role}_patterns"] = pattern_score
for rule in self.ROLE_RULES:
trigger_score = sum(1 for trigger in rule.triggers if trigger.lower() in content_lower)
if trigger_score > 0:
scores[rule.role] += trigger_score * 0.7
details["scores"][f"{rule.role}_triggers"] = trigger_score
for rule in self.ROLE_RULES:
if scores[rule.role] > 0:
scores[rule.role] *= 1 + rule.priority * 0.01
max_role = Role.AI_ENGINEER
max_score = 0.0
for role, score in scores.items():
if score > max_score:
max_score = score
max_role = role
elif score == max_score and score > 0:
role_priority = 0
max_role_priority = 0
for rule in self.ROLE_RULES:
if rule.role == role:
role_priority = rule.priority
if rule.role == max_role:
max_role_priority = rule.priority
if role_priority > max_role_priority:
max_role = role
confidence = min(max_score / 3.0, 1.0) if max_score > 0 else 0.5
details["matched_rules"] = [
rule.name
for rule in self.ROLE_RULES
if scores[rule.role] > 0
]
details["final_scores"] = {role.value: score for role, score in scores.items()}
details["reasons"].append("基于关键词和模式匹配")
return max_role, confidence, details
def _should_escalate_to_boss(self, analysis: dict[str, Any]) -> bool:
dev_days = analysis.get("resource_cost", {}).get("dev_days", 0)
if dev_days >= 5:
return True
risk_level = analysis.get("risk_level", "")
if risk_level in ["CRITICAL", "HIGH"]:
return True
is_architecture = analysis.get("is_architecture_change", False)
if is_architecture:
return True
return False
def get_role_info(self, role: Role) -> dict[str, Any]:
for rule in self.ROLE_RULES:
if rule.role == role:
return {
"role": role.value,
"name": rule.name,
"description": rule.description,
"keywords": rule.keywords,
"priority": rule.priority,
}
return {}
def get_all_roles(self) -> list[dict[str, Any]]:
return [
{
"role": rule.role.value,
"name": rule.name,
"description": rule.description,
"priority": rule.priority,
}
for rule in sorted(self.ROLE_RULES, key=lambda item: -item.priority)
]
class RoutingResult(BaseModel):
"""路由结果模型。"""
role: str
confidence: float
details: dict[str, Any]
signal_id: Optional[str] = None
timestamp: float = Field(default_factory=lambda: now_cn_aware().timestamp())
_global_router: Optional[RoleRouter] = None
def get_router() -> RoleRouter:
"""获取全局路由器实例。"""
global _global_router
if _global_router is None:
_global_router = RoleRouter()
return _global_router
def route_signal(
signal_content: str,
analysis: Optional[dict[str, Any]] = None,
signal_id: Optional[str] = None,
) -> RoutingResult:
"""路由信号到对应角色。"""
router = get_router()
role, confidence, details = router.route(signal_content, analysis)
return RoutingResult(
role=role.value,
confidence=confidence,
details=details,
signal_id=signal_id,
)