|
|
""" |
|
|
6️⃣ OPERATOR INTERACTIONS - Human-aware security |
|
|
Purpose: Learns human behavior patterns for better cohabitation. |
|
|
""" |
|
|
|
|
|
from sqlalchemy import Column, String, DateTime, JSON, Integer, Float, Boolean, Text, CheckConstraint, Index, ForeignKey |
|
|
from sqlalchemy.dialects.postgresql import UUID |
|
|
from sqlalchemy.orm import relationship |
|
|
from sqlalchemy.sql import func |
|
|
import uuid |
|
|
|
|
|
from database.models.base import Base |
|
|
|
|
|
class OperatorInteraction(Base): |
|
|
__tablename__ = "operator_interactions" |
|
|
|
|
|
|
|
|
interaction_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
|
|
interaction_time = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) |
|
|
|
|
|
|
|
|
interaction_type = Column(String(30), nullable=False) |
|
|
|
|
|
|
|
|
operator_hash = Column(String(64), nullable=False) |
|
|
operator_role = Column(String(20), nullable=False) |
|
|
|
|
|
|
|
|
target_type = Column(String(30), nullable=False) |
|
|
target_id = Column(String(100), nullable=False) |
|
|
|
|
|
|
|
|
action_taken = Column(String(50), nullable=False) |
|
|
action_parameters = Column(JSON, nullable=False, default=dict, server_default="{}") |
|
|
|
|
|
|
|
|
autonomous_decision_id = Column(UUID(as_uuid=True), ForeignKey("autonomous_decisions.decision_id")) |
|
|
autonomous_decision = relationship("AutonomousDecision") |
|
|
system_state_at_interaction = Column(String(20), nullable=False) |
|
|
|
|
|
|
|
|
decision_latency_ms = Column(Integer) |
|
|
review_duration_ms = Column(Integer) |
|
|
|
|
|
|
|
|
was_override = Column(Boolean, nullable=False, default=False, server_default="false") |
|
|
override_reason = Column(Text) |
|
|
override_confidence = Column(Float) |
|
|
|
|
|
|
|
|
outcome_recorded = Column(Boolean, nullable=False, default=False, server_default="false") |
|
|
outcome_notes = Column(Text) |
|
|
|
|
|
|
|
|
__table_args__ = ( |
|
|
CheckConstraint( |
|
|
"interaction_type IN ('policy_override', 'model_governance_change', 'security_state_adjustment', 'decision_review', 'system_configuration', 'audit_review')", |
|
|
name="ck_interaction_type" |
|
|
), |
|
|
CheckConstraint( |
|
|
"operator_role IN ('executive', 'observer', 'analyst', 'engineer', 'admin')", |
|
|
name="ck_operator_role" |
|
|
), |
|
|
CheckConstraint( |
|
|
"system_state_at_interaction IN ('normal', 'elevated', 'emergency', 'degraded')", |
|
|
name="ck_interaction_system_state" |
|
|
), |
|
|
CheckConstraint( |
|
|
"override_confidence IS NULL OR (override_confidence >= 0.0 AND override_confidence <= 1.0)", |
|
|
name="ck_override_confidence" |
|
|
), |
|
|
Index("idx_interactions_time", "interaction_time"), |
|
|
Index("idx_interactions_operator", "operator_hash"), |
|
|
Index("idx_interactions_type", "interaction_type"), |
|
|
Index("idx_interactions_override", "was_override"), |
|
|
Index("idx_interactions_decision", "autonomous_decision_id"), |
|
|
) |
|
|
|
|
|
def __repr__(self): |
|
|
return f"<OperatorInteraction {self.interaction_type} by {self.operator_role}>" |
|
|
|
|
|
def to_dict(self): |
|
|
"""Convert to dictionary for serialization""" |
|
|
return { |
|
|
"interaction_id": str(self.interaction_id), |
|
|
"interaction_time": self.interaction_time.isoformat() if self.interaction_time else None, |
|
|
"interaction_type": self.interaction_type, |
|
|
"operator_role": self.operator_role, |
|
|
"target_type": self.target_type, |
|
|
"target_id": self.target_id, |
|
|
"action_taken": self.action_taken, |
|
|
"was_override": self.was_override, |
|
|
"decision_latency_ms": self.decision_latency_ms, |
|
|
"review_duration_ms": self.review_duration_ms, |
|
|
"outcome_recorded": self.outcome_recorded |
|
|
} |
|
|
|
|
|
@classmethod |
|
|
def get_operator_interactions(cls, session, operator_hash: str, limit: int = 50): |
|
|
"""Get interactions by specific operator""" |
|
|
return ( |
|
|
session.query(cls) |
|
|
.filter(cls.operator_hash == operator_hash) |
|
|
.order_by(cls.interaction_time.desc()) |
|
|
.limit(limit) |
|
|
.all() |
|
|
) |
|
|
|
|
|
@classmethod |
|
|
def get_recent_overrides(cls, session, limit: int = 100): |
|
|
"""Get recent override interactions""" |
|
|
return ( |
|
|
session.query(cls) |
|
|
.filter(cls.was_override == True) |
|
|
.order_by(cls.interaction_time.desc()) |
|
|
.limit(limit) |
|
|
.all() |
|
|
) |
|
|
|
|
|
@classmethod |
|
|
def get_operator_statistics(cls, session, operator_hash: str): |
|
|
"""Get statistics for an operator""" |
|
|
from sqlalchemy import func as sql_func |
|
|
|
|
|
stats = session.query( |
|
|
sql_func.count(cls.interaction_id).label("total_interactions"), |
|
|
sql_func.avg(cls.decision_latency_ms).label("avg_decision_latency"), |
|
|
sql_func.avg(cls.review_duration_ms).label("avg_review_duration"), |
|
|
sql_func.sum(sql_func.cast(cls.was_override, Integer)).label("total_overrides") |
|
|
).filter(cls.operator_hash == operator_hash).first() |
|
|
|
|
|
return { |
|
|
"total_interactions": stats.total_interactions or 0, |
|
|
"avg_decision_latency": float(stats.avg_decision_latency or 0), |
|
|
"avg_review_duration": float(stats.avg_review_duration or 0), |
|
|
"total_overrides": stats.total_overrides or 0 |
|
|
} |
|
|
|
|
|
def record_override(self, reason: str, confidence: float = None): |
|
|
"""Record that this was an override""" |
|
|
self.was_override = True |
|
|
self.override_reason = reason |
|
|
if confidence is not None: |
|
|
self.override_confidence = confidence |
|
|
|
|
|
def record_outcome(self, notes: str): |
|
|
"""Record outcome of this interaction""" |
|
|
self.outcome_recorded = True |
|
|
self.outcome_notes = notes |
|
|
|
|
|
def get_hesitation_score(self) -> float: |
|
|
"""Calculate hesitation score (0-1, higher = more hesitant)""" |
|
|
if not self.review_duration_ms: |
|
|
return 0.0 |
|
|
|
|
|
|
|
|
normalized = min(self.review_duration_ms / (5 * 60 * 1000), 1.0) |
|
|
|
|
|
|
|
|
if self.decision_latency_ms and self.decision_latency_ms > 30000: |
|
|
normalized = min(normalized + 0.3, 1.0) |
|
|
|
|
|
return normalized |
|
|
|