Spaces:
Running
Running
| """ | |
| π± Imperial Emotion Utility β λ¨μΌ μ§μ€ μ μ₯μ | |
| κ°μ ν€μλ λͺ©λ‘, νκ·Έ μΆμΆ, νκ·Έ μ κ±° ν¨μλ₯Ό μ¬κΈ°μλ§ μ μν©λλ€. | |
| chat_routes.py / web_server.py / discord_service.py λͺ¨λ μ΄ λͺ¨λμ μν¬νΈνμμμ€. | |
| """ | |
| import re | |
| import time | |
| import logging | |
| _emotion_logger = logging.getLogger(__name__) | |
| EMOTION_KEYWORDS = [ | |
| 'happy', 'sad', 'angry', 'surprised', 'disgust', 'smirk', 'neutral', | |
| 'peaceful', 'excited', 'serious', 'thinking', 'love', 'shy', 'fear', | |
| 'fearful', 'joy', 'joyful', 'pain', 'tired', 'bored', 'playful', | |
| 'confident', 'anxious', 'relief', 'devoted', 'loyal', 'proud', 'sympathetic', | |
| ] | |
| # [EMOTION:xxx] / [EMOTION Board:xxx] | |
| EMOTION_BOARD_RE = re.compile(r'\[EMOTION(?:\s+Board)?:([^\]]*)\]', re.IGNORECASE) | |
| # λ κ±°μ μΈλΌμΈ νκ·Έ [happy] λ± | |
| INLINE_EMOTION_RE = re.compile( | |
| r'\[(' + '|'.join(EMOTION_KEYWORDS) + r')\]', | |
| re.IGNORECASE, | |
| ) | |
| # ν둬ννΈμ© νμ© κ°μ λͺ©λ‘ λ¬Έμμ΄ | |
| ALLOWED_EMOTION_LIST = ', '.join(EMOTION_KEYWORDS) | |
| # π κ°λ μ±Β·κ°μ μ§μΉ¨μ 'νλμ½λ© ν΄λ°±'μ μ κ±°λ¨(2026-06-29). | |
| # κ³Όκ±° λΈλ μΈμ΄ Aegisλ₯Ό λͺ» μ½μΌλ©΄(ν ν° λλ½ 401 λ±) μ‘°μ©ν μ΄ νλμ½λ© κ°μ μ£Όμ ν΄, | |
| # 'κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ' νΈμ§μ΄ μ λ¨Ήνλ footgunμ΄μλ€. μ΄μ μ§μΉ¨ ν μ€νΈμ 'μ μΌν μ§μ€'μ | |
| # Aegis DB(κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ)μ΄λ©°, μ‘°ν μ€ν¨ μ λΉ κ°(μ£Όμ μ ν¨)μΌλ‘ μ€ν¨λ₯Ό λλ¬λΈλ€. | |
| # (DB μλ κΈ°λ³Έκ°μ Aegis system_views.DEFAULT_*κ° λ΄λΉ β UI μ΄κΈ° μ½ν μΈ .) | |
| def _append_emotion_example(instruction: str, example: str | None) -> str: | |
| """κ°μ μ§μΉ¨ λ³Έλ¬Έ λμ λ§ν¬ μμ ν μ€μ λ§λΆμΈλ€(μμκ° λΉλ©΄ κ·Έλλ‘). | |
| DBμ emotion_example(ν κΈ ON)μ λ³Έλ¬Έμ λΆμΌ λ μ¬μ©.""" | |
| ex = (example or "").strip() | |
| if not ex: | |
| return instruction | |
| return instruction.rstrip() + f"\n (μ: \"{ex}\")" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # π± λμ μ§μΉ¨ λ‘λ (Aegis REST API β λ©λͺ¨λ¦¬ μΊμ β νλμ½λ© ν΄λ°±) | |
| # νΈμ§ μ¦μμ±μ '무ν¨ν ν 'μ΄ λ΄λΉνλ€: Aegisκ° κ³΅ν΅ μ§μΉ¨ μ μ₯ μ§ν | |
| # POST /api/shadow/guidelines/invalidate λ‘ μΊμλ₯Ό λΉμ, νΈμ§μ΄ 'λ€μ λ©μμ§ 1λ²'μ λ°λ‘ | |
| # λ°μλλ€(TTL λ§λ£λ₯Ό κΈ°λ€λ¦΄ νμ μμ β 'λ λ² λ§ κ±ΈκΈ°' λΆνΈ ν΄μ). | |
| # λ°λΌμ TTLμ 'λν μ€ μ€λ³΅ νΈμΆ μ κ° + ν μ΄ λΏμ§ μλ κ²½μ°(λΈλ μΈ μ¬κΈ°λ/ν΄λΌμ°λ NAT)μ | |
| # μμ λ§' μν λ§ νλ€. DB(cache_ttl_seconds, κ΄λ¦¬μ νΈμ§)κ° μ°μ μ΄λ©° κΈ°λ³Έ 60μ΄. | |
| # μμ 무μΊμλ λ§€ λ©μμ§λ§λ€ 3μ’ (κ°λ μ±/κ°μ /κ²μμ μ± )μ Aegisλ‘ GETν΄ λΉν¨μ¨μ μ΄λ€. | |
| # Aegis λ―ΈκΈ°λ μμλ νλμ½λ© ν΄λ°±μΌλ‘ μ μ μλνλ€. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _GUIDELINES_CACHE: dict = {} | |
| _GUIDELINES_TTL: float = 60.0 # κΈ°λ³Έ 60μ΄(μμ λ§). μ¦μμ±μ 무ν¨ν ν μ΄ λ΄λΉ. DBκ°μ΄ μ°μ . | |
| # β±οΈ DB(κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ)μμ λ°μ TTLμ λ°νμμ 보κ΄. 첫 μ‘°ν μ μ κΈ°λ³Έκ° μ¬μ©. | |
| _RUNTIME_TTL: float | None = None | |
| def _eff_ttl() -> float: | |
| """μΊμ μ ν¨ μκ°(μ΄). DBμμ λ°μ κ° μ°μ , μμΌλ©΄ κΈ°λ³Έκ°.""" | |
| return _RUNTIME_TTL if _RUNTIME_TTL is not None else _GUIDELINES_TTL | |
| def invalidate_guidelines_cache() -> None: | |
| """κ³΅ν΅ μ§μΉ¨ μΊμλ₯Ό μ¦μ λΉμ΄λ€. Aegisκ° μ§μΉ¨ μ μ₯ μ§ν 무ν¨ν ν μΌλ‘ νΈμΆ β | |
| λ€μ λ©μμ§ 1λ²μ μ μ§μΉ¨μ΄ λ°λ‘ λ°μλλ€(νΈμ§ ν 'λ λ² λ§ κ±ΈκΈ°' λΆνΈ ν΄μ). | |
| TTLμ μμ λ§μΌλ‘λ§ λ¨κ³ , νμμ μ΄ ν μ΄ κ°±μ μ λ΄λΉνλ€.""" | |
| _GUIDELINES_CACHE.clear() | |
| _emotion_logger.info("[Guidelines] μΊμ 무ν¨ν β λ€μ μ‘°νμμ μ΅μ μ§μΉ¨μ λ€μ κ°μ Έμ΅λλ€.") | |
| _FETCH_FAIL_CACHE: dict = {} # aegis_url β μ€ν¨ μκ° (λ€κ±°ν°λΈ μΊμ) | |
| def _fetch_guidelines_from_aegis(aegis_url: str) -> dict | None: | |
| """Aegis REST APIμμ κ³΅ν΅ μ§μΉ¨μ κ°μ Έμ΅λλ€. μ€ν¨ μ None λ°ν. | |
| β οΈ GET /api/system/guidelines λ μΈμ¦μ μꡬνλ€(_require_auth). ν ν° μμ΄ νΈμΆνλ©΄ | |
| 401 β None β νλμ½λ© ν΄λ°±μ΄ μ°μ¬, 'κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ' νΈμ§μ΄ λΈλ μΈμ μ ν λ°μλμ§ | |
| μλλ€. λ°λμ μλΉμ€ ν ν°μ μ€μ΄ 보λΈλ€(μ μ₯ κ²½λ‘ get_job_perf_flagsμ λμΌ ν¨ν΄).""" | |
| global _RUNTIME_TTL | |
| # π‘οΈ μ€ν¨ λ€κ±°ν°λΈ μΊμ β 401/λ€νΈμν¬ μ€ν¨κ° λ°λ³΅λλ λμ λͺ¨λ μλΉμ | |
| # (retrieval_policy/emotion/readability)κ° λ§€ ν΄ ν΄λΌμ°λ μ볡μ λ°λ³΅νμ§ μκ². | |
| _fail_at = _FETCH_FAIL_CACHE.get(aegis_url) | |
| if _fail_at and (time.time() - _fail_at) < _eff_ttl(): | |
| return None | |
| try: | |
| import os | |
| import requests | |
| headers = {} | |
| tok = os.environ.get("IMPERIAL_API_TOKEN") or os.environ.get("SHADOWBRAIN_TOKEN") | |
| if tok: | |
| headers["X-Imperial-Token"] = tok | |
| headers["Authorization"] = f"Bearer {tok}" | |
| url = aegis_url.rstrip('/') + '/api/system/guidelines' | |
| resp = requests.get(url, headers=headers, timeout=3) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| # β±οΈ κ΄λ¦¬μμμ μ€μ ν μΊμ TTLμ λ°νμμ λ°μ(λ€μ μΊμ νμ λΆν° μ μ©). | |
| if isinstance(data, dict) and data.get('cache_ttl_seconds') is not None: | |
| try: | |
| _RUNTIME_TTL = float(data['cache_ttl_seconds']) | |
| except (TypeError, ValueError): | |
| pass | |
| _FETCH_FAIL_CACHE.pop(aegis_url, None) | |
| return data # {"readability": "...", "emotion": "...", "cache_ttl_seconds": N} | |
| _emotion_logger.debug(f"[Guidelines] Aegis HTTP {resp.status_code} β λ€κ±°ν°λΈ μΊμ") | |
| except Exception as e: | |
| _emotion_logger.debug(f"[Guidelines] Aegis μ‘°ν μ€ν¨ (ν΄λ°± μ¬μ©): {e}") | |
| _FETCH_FAIL_CACHE[aegis_url] = time.time() | |
| return None | |
| def get_dynamic_retrieval_policy(aegis_url: str | None = None) -> dict: | |
| """DB κ³΅ν΅ κ²μ μ μ± . μ‘°ν μ€ν¨/λλ½ μ μλ κ²μμ μ€λ¨νλ€.""" | |
| if not aegis_url: | |
| return {} | |
| cache_key = f"{aegis_url}:retrieval_policy" | |
| cached = _GUIDELINES_CACHE.get(cache_key) | |
| if cached and (time.time() - cached['ts']) < _eff_ttl(): | |
| return cached['value'] | |
| data = _fetch_guidelines_from_aegis(aegis_url) | |
| value = data.get('retrieval_policy') if data else None | |
| if isinstance(value, dict): | |
| _GUIDELINES_CACHE[cache_key] = {'value': value, 'ts': time.time()} | |
| return value | |
| # π‘οΈ λ€κ±°ν°λΈ μΊμ β Aegis μ€ν¨/λλ½λ TTL λμ κΈ°μ΅. μ νλ©΄ μλ κ°μ§ ν¬νΌ | |
| # (_tool_policy/_intent_cfg λ±)κ° λ§€ ν΄ ν΄λΌμ°λ μ볡(~840ms)μ λ°λ³΅ν΄ TTFTλ₯Ό | |
| # μ μ΄μ© νμ΄λ€(2026-07-10 μ€μΈ‘). | |
| _GUIDELINES_CACHE[cache_key] = {'value': {}, 'ts': time.time()} | |
| return {} | |
| def get_dynamic_emotion_instruction(aegis_url: str | None = None) -> str: | |
| """ | |
| κ°μ μ°κΈ° μ§μΉ¨μ λ°νν©λλ€. DB(Aegis 'κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ')κ° 'μ μΌν μ§μ€'. | |
| - aegis_url μ 곡 + μ‘°ν μ±κ³΅ μ: DB κ°(+ ν κΈ μΌμ‘μΌλ©΄ λ§ν¬ μμ) λ°ν. | |
| - aegis_url μκ±°λ μ‘°ν μ€ν¨ μ: λΉ λ¬Έμμ΄(μ£Όμ μ ν¨). νλμ½λ© ν΄λ°± μμ β | |
| μμ μ ν΄λ°±μ΄ 'μ‘°μ©ν λ€λ₯Έ κ°'μ μ£Όμ ν΄ κ΄λ¦¬μ νΈμ§μ΄ μ λ¨Ήνλ footgunμ΄μλ€. | |
| """ | |
| if not aegis_url: | |
| return "" | |
| cache_key = f"{aegis_url}:emotion" | |
| cached = _GUIDELINES_CACHE.get(cache_key) | |
| if cached and (time.time() - cached['ts']) < _eff_ttl(): | |
| return cached['value'] | |
| data = _fetch_guidelines_from_aegis(aegis_url) | |
| if data and 'emotion' in data and data['emotion'] is not None: | |
| value = data['emotion'] | |
| # π λ§ν¬ μμλ λ³λ νλ + ν κΈ(emotion_example_enabled). μΌμ‘μ λλ§ λ§λΆμΈλ€. | |
| if data.get('emotion_example_enabled', True): | |
| value = _append_emotion_example(value, data.get('emotion_example')) | |
| _GUIDELINES_CACHE[cache_key] = {'value': value, 'ts': time.time()} | |
| return value | |
| return "" | |
| def get_dynamic_readability_instruction(aegis_url: str | None = None) -> str: | |
| """ | |
| κ°λ μ± μ§μΉ¨μ λ°νν©λλ€. DB(Aegis 'κ³΅ν΅ μ§μΉ¨ κ΄λ¦¬μ')κ° 'μ μΌν μ§μ€'. | |
| - aegis_url μ 곡 + μ‘°ν μ±κ³΅ μ: DB κ° λ°ν. | |
| - aegis_url μκ±°λ μ‘°ν μ€ν¨ μ: λΉ λ¬Έμμ΄(μ£Όμ μ ν¨). νλμ½λ© ν΄λ°± μμ. | |
| """ | |
| if not aegis_url: | |
| return "" | |
| cache_key = f"{aegis_url}:readability" | |
| cached = _GUIDELINES_CACHE.get(cache_key) | |
| if cached and (time.time() - cached['ts']) < _eff_ttl(): | |
| return cached['value'] | |
| data = _fetch_guidelines_from_aegis(aegis_url) | |
| if data and 'readability' in data and data['readability'] is not None: | |
| value = data['readability'] | |
| _GUIDELINES_CACHE[cache_key] = {'value': value, 'ts': time.time()} | |
| return value | |
| return "" | |
| def extract_emotion(text: str, default: str = 'peaceful') -> str: | |
| """AI μλ΅μμ κ°μ κ°μ μΆμΆν©λλ€. [EMOTION:xxx] μ°μ , λ κ±°μ μΈλΌμΈ ν΄λ°±.""" | |
| if not text: | |
| return default | |
| matches = EMOTION_BOARD_RE.findall(text) | |
| if matches: | |
| return matches[-1].strip().lower() | |
| matches = INLINE_EMOTION_RE.findall(text) | |
| if matches: | |
| return matches[-1].lower() | |
| return default | |
| def strip_emotion_tags(text: str) -> str: | |
| """νμμ© ν μ€νΈμμ λͺ¨λ κ°μ νκ·Έλ₯Ό μ κ±°ν©λλ€. extract_emotion ν νΈμΆνμμμ€.""" | |
| t = EMOTION_BOARD_RE.sub('', text) | |
| t = INLINE_EMOTION_RE.sub('', t) | |
| return t.strip() | |