"""Family configuration loader — Single-tenant only. Loads family data from YAML and provides pattern matching for inference. No multi-tenant support — hardcoded to Hoffmann household. """ import re import yaml from pathlib import Path from typing import Optional # SINGLE-TENANT: Hardcoded family DEFAULT_FAMILY_ID = "hoffmann" # Find config relative to this file (icarus/core/family_loader.py → icarus/config/families/) FAMILY_CONFIG_PATH = Path(__file__).parent.parent / "config" / "families" / f"{DEFAULT_FAMILY_ID}.yaml" class FamilyConfig: """Single-tenant family configuration.""" def __init__(self, family_id: str = DEFAULT_FAMILY_ID): """Load family configuration from YAML. Args: family_id: Must be "hoffmann". Single-tenant constraint. """ if family_id != DEFAULT_FAMILY_ID: raise ValueError(f"Single-tenant only. Family must be '{DEFAULT_FAMILY_ID}'.") self.family_id = family_id self._config = self._load_config() self._compiled_rules = self._compile_rules() def _load_config(self) -> dict: """Load and parse YAML configuration.""" if not FAMILY_CONFIG_PATH.exists(): raise FileNotFoundError(f"Family config not found: {FAMILY_CONFIG_PATH}") with open(FAMILY_CONFIG_PATH) as f: return yaml.safe_load(f) def _compile_rules(self) -> list: """Pre-compile regex patterns for fast matching.""" compiled = [] for rule in self.inference_rules: try: compiled.append({ **rule, "_regex": re.compile(rule["pattern"], re.IGNORECASE) }) except re.error as e: print(f"Warning: Invalid regex in rule {rule['id']}: {e}") return compiled @property def members(self) -> list[dict]: """Return all family members.""" return self._config.get("members", []) @property def inference_rules(self) -> list[dict]: """Return pattern matching rules.""" return self._config.get("inference_rules", []) @property def fallback_threshold(self) -> float: """Confidence threshold for asking user.""" return self._config.get("telemetry", {}).get("fallback_threshold", 0.70) def get_member(self, member_id: str) -> Optional[dict]: """Get member by ID.""" for member in self.members: if member.get("id") == member_id: return member return None def infer_recipients(self, text: str) -> list[dict]: """Infer which family members a document concerns. Uses deterministic pattern matching (no LLM). Returns: [{member_id, confidence, rule_id, description}] """ text_lower = text.lower() matches = [] matched_ids = set() for rule in self._compiled_rules: if rule["_regex"].search(text): for member_id in rule["assign_to"]: if member_id not in matched_ids: matches.append({ "member_id": member_id, "member": self.get_member(member_id), "confidence": rule["confidence"], "rule_id": rule["id"], "description": rule["description"] }) matched_ids.add(member_id) return matches def build_context_prompt(self) -> str: """Build family context string for LLM prompts.""" lines = ["FAMILY MEMBERS:"] for member in self.members: if member.get("current_grade"): # Kids for school docs lines.append(f"- {member['name']} ({member.get('nickname', 'no nickname')}) Grade {member['current_grade']}, Teacher: {member.get('teacher', 'unknown')}") elif member.get("is_parent"): lines.append(f"- {member['name']} (parent)") lines.append("") lines.append("INFERENCE RULES:") for rule in self.inference_rules: lines.append(f"- {rule['description']} → {', '.join(rule['assign_to'])} (confidence: {rule['confidence']})") return "\n".join(lines) def get_telemetry_config(self) -> dict: """Get telemetry/logging configuration.""" return self._config.get("telemetry", {}) # Singleton instance _family_config: Optional[FamilyConfig] = None def get_family_config() -> FamilyConfig: """Get singleton family configuration. Single-tenant: Always returns Hoffmann family config. """ global _family_config if _family_config is None: _family_config = FamilyConfig() return _family_config def reset_family_config(): """Reset singleton (useful for testing).""" global _family_config _family_config = None