taemin1980's picture
πŸ”± Imperial Deployment: Shadow Brain Core ignition (part 2)
de2eefc verified
Raw
History Blame Contribute Delete
10.2 kB
"""
πŸ”± 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()