181 lines
5.8 KiB
Python
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,
|
|
)
|