Files
timmy-config/wizards/allegro/gofai/rule_engine.py
2026-03-31 20:02:01 +00:00

348 lines
11 KiB
Python

#!/usr/bin/env python3
"""
GOFAI Rule Engine for Hermes Fleet
Forward-chaining rule engine for principle enforcement and reasoning
"""
import json
import re
from dataclasses import dataclass, field
from typing import List, Dict, Callable, Optional, Any
from enum import Enum
class ActionType(Enum):
ALLOW = "allow"
BLOCK = "block"
WARN = "warn"
REQUIRE_APPROVAL = "require_approval"
LOG = "log"
@dataclass
class Rule:
"""A rule in the rule engine"""
id: str
name: str
condition: str # Python expression or pattern
action: ActionType
explanation: str
priority: int = 1
enabled: bool = True
category: str = "general"
def to_dict(self) -> Dict:
return {
'id': self.id,
'name': self.name,
'condition': self.condition,
'action': self.action.value,
'explanation': self.explanation,
'priority': self.priority,
'enabled': self.enabled,
'category': self.category
}
class RuleContext:
"""Context for rule evaluation"""
def __init__(self, **kwargs):
self.data = kwargs
self.history = []
def get(self, key: str, default=None):
return self.data.get(key, default)
def set(self, key: str, value):
self.data[key] = value
def log(self, message: str):
self.history.append(message)
class RuleEngine:
"""
Forward-chaining rule engine
Evaluates rules against context and executes actions
"""
def __init__(self):
self.rules: Dict[str, Rule] = {}
self.actions: Dict[str, Callable] = {}
self.rule_order: List[str] = []
# Register default actions
self._register_default_actions()
def _register_default_actions(self):
"""Register built-in action handlers"""
def allow_action(ctx: RuleContext, rule: Rule) -> Dict:
return {'allowed': True, 'reason': rule.explanation}
def block_action(ctx: RuleContext, rule: Rule) -> Dict:
return {
'allowed': False,
'reason': rule.explanation,
'rule_id': rule.id,
'action': 'block'
}
def warn_action(ctx: RuleContext, rule: Rule) -> Dict:
return {
'allowed': True,
'warning': rule.explanation,
'rule_id': rule.id,
'action': 'warn'
}
def require_approval_action(ctx: RuleContext, rule: Rule) -> Dict:
return {
'allowed': False,
'reason': rule.explanation,
'requires_approval': True,
'rule_id': rule.id
}
def log_action(ctx: RuleContext, rule: Rule) -> Dict:
ctx.log(f"Rule {rule.id} triggered: {rule.explanation}")
return {'allowed': True, 'logged': True}
self.actions = {
ActionType.ALLOW.value: allow_action,
ActionType.BLOCK.value: block_action,
ActionType.WARN.value: warn_action,
ActionType.REQUIRE_APPROVAL.value: require_approval_action,
ActionType.LOG.value: log_action,
}
def add_rule(self, rule: Rule) -> None:
"""Add a rule to the engine"""
self.rules[rule.id] = rule
# Re-sort rules by priority
self.rule_order = sorted(
self.rules.keys(),
key=lambda r: self.rules[r].priority
)
def load_soul_principles(self, soul_md_content: str) -> List[Rule]:
"""
Parse SOUL.md and convert to rules
"""
rules = []
# Pattern matching for SOUL.md structure
sections = {
'must_do': r'Must do:([^#]+)',
'must_not_do': r'Must not do:([^#]+)',
'belief': r'Belief:([^#]+)',
}
for category, pattern in sections.items():
matches = re.findall(pattern, soul_md_content, re.IGNORECASE)
for i, match in enumerate(matches):
lines = [l.strip() for l in match.strip().split('\n') if l.strip()]
for line in lines:
if line.startswith('-') or line.startswith('*'):
principle = line[1:].strip()
rule = self._principle_to_rule(principle, category, i)
rules.append(rule)
self.add_rule(rule)
return rules
def _principle_to_rule(self, principle: str, category: str, index: int) -> Rule:
"""Convert a SOUL principle to a rule"""
# Extract keywords from principle
principle_lower = principle.lower()
# Determine action based on category
if category == 'must_not_do':
action = ActionType.BLOCK
# Check if it should just warn instead
if 'avoid' in principle_lower or 'prefer' in principle_lower:
action = ActionType.WARN
elif category == 'must_do':
action = ActionType.REQUIRE_APPROVAL
else:
action = ActionType.LOG
# Build condition from principle text
# This is a simplified version - sophisticated version would use NLP
keywords = self._extract_keywords(principle)
condition = f'action.contains_any({keywords})'
return Rule(
id=f'soul_{category}_{index}',
name=f'SOUL: {principle[:50]}...',
condition=condition,
action=action,
explanation=principle,
priority=1 if category == 'must_not_do' else 2,
category=category
)
def _extract_keywords(self, text: str) -> List[str]:
"""Extract significant keywords from text"""
# Simple keyword extraction
words = text.lower().split()
# Filter out common words
stopwords = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for'}
keywords = [w.strip('.,;:!?') for w in words if w not in stopwords and len(w) > 3]
return list(set(keywords))[:5] # Top 5 unique keywords
def evaluate(self, action_description: str, context: Optional[RuleContext] = None) -> Dict:
"""
Evaluate an action against all rules
Returns decision and explanations
"""
if context is None:
context = RuleContext(action=action_description)
else:
context.set('action', action_description)
results = []
blocked = False
warnings = []
# Evaluate rules in priority order
for rule_id in self.rule_order:
rule = self.rules[rule_id]
if not rule.enabled:
continue
# Check if condition matches
if self._condition_matches(rule.condition, action_description, context):
# Execute action
handler = self.actions.get(rule.action.value, self.actions['log'])
result = handler(context, rule)
results.append(result)
if result.get('action') == 'block':
blocked = True
if result.get('action') == 'warn':
warnings.append(result.get('warning'))
# For BLOCK actions, stop processing
if blocked and rule.priority <= 1:
break
return {
'allowed': not blocked,
'action': action_description,
'violations': [r for r in results if r.get('action') == 'block'],
'warnings': warnings,
'log': context.history
}
def _condition_matches(self, condition: str, action: str, ctx: RuleContext) -> bool:
"""
Check if condition matches action
Simplified version - full version would parse and evaluate properly
"""
action_lower = action.lower()
# Parse condition
if 'contains_any' in condition:
# Extract keywords from condition
match = re.search(r'\[([^\]]+)\]', condition)
if match:
keywords_str = match.group(1)
keywords = [k.strip().strip("'") for k in keywords_str.split(',')]
return any(kw in action_lower for kw in keywords)
# Simple substring match
if condition.lower() in action_lower:
return True
return False
def get_rules_by_category(self, category: str) -> List[Rule]:
"""Get all rules in a category"""
return [r for r in self.rules.values() if r.category == category]
def export_rules(self) -> str:
"""Export all rules as JSON"""
rules_list = [r.to_dict() for r in self.rules.values()]
return json.dumps(rules_list, indent=2)
def import_rules(self, json_str: str) -> int:
"""Import rules from JSON"""
rules_data = json.loads(json_str)
count = 0
for data in rules_data:
rule = Rule(
id=data['id'],
name=data['name'],
condition=data['condition'],
action=ActionType(data['action']),
explanation=data['explanation'],
priority=data.get('priority', 1),
enabled=data.get('enabled', True),
category=data.get('category', 'general')
)
self.add_rule(rule)
count += 1
return count
# Pre-built rule sets for children
CHILD_SAFETY_RULES = [
Rule(
id='child_no_delete',
name='Children cannot delete critical files',
condition='delete and (critical or system)',
action=ActionType.REQUIRE_APPROVAL,
explanation='Children must request approval before deleting critical system files',
priority=0,
category='safety'
),
Rule(
id='child_no_network',
name='Children cannot modify network config',
condition='network or firewall or iptables',
action=ActionType.BLOCK,
explanation='Network configuration changes are restricted',
priority=0,
category='safety'
),
Rule(
id='child_report_error',
name='Children must report errors',
condition='error and not report',
action=ActionType.WARN,
explanation='When encountering errors, children should report to father',
priority=2,
category='behavior'
),
]
FLEET_COORDINATION_RULES = [
Rule(
id='coord_no_conflict',
name='Avoid task conflicts',
condition='same_task and same_resource',
action=ActionType.REQUIRE_APPROVAL,
explanation='Multiple wizards working on same task/resource requires coordination',
priority=1,
category='coordination'
),
Rule(
id='coord_report_complete',
name='Report task completion',
condition='complete and not report',
action=ActionType.WARN,
explanation='Completed tasks should be reported to father-messages/',
priority=2,
category='coordination'
),
]
def create_child_rule_engine() -> RuleEngine:
"""Create a rule engine pre-configured for child wizards"""
engine = RuleEngine()
for rule in CHILD_SAFETY_RULES:
engine.add_rule(rule)
for rule in FLEET_COORDINATION_RULES:
engine.add_rule(rule)
return engine