""" ๐Ÿงฌ NPC Memory Evolution System โ€” ์ž๊ฐ€์ง„ํ™” ์˜๊ตฌํ•™์Šต =================================================== ๊ฐ NPC๋ณ„ ๋…๋ฆฝ์  3๋‹จ๊ณ„ ๊ธฐ์–ต + ์ž๊ฐ€์ง„ํ™” ์—”์ง„ ๊ธฐ์–ต ๊ณ„์ธต: ๐Ÿ“Œ ๋‹จ๊ธฐ ๊ธฐ์–ต (Short-term): ์ตœ๊ทผ 1์‹œ๊ฐ„ ํ™œ๋™, ๋ฐฉ๊ธˆ ๋ณธ ๋‰ด์Šค, ํ˜„์žฌ ํฌ์ง€์…˜ (์ž๋™ ๋งŒ๋ฃŒ) ๐Ÿ“’ ์ค‘๊ธฐ ๊ธฐ์–ต (Medium-term): ์ตœ๊ทผ 7์ผ ํ•™์Šต, ์„ฑ๊ณต/์‹คํŒจ ํŒจํ„ด, ๋‰ด์Šค ํŠธ๋ Œ๋“œ (์ฃผ๊ธฐ์  ์••์ถ•) ๐Ÿ“š ์žฅ๊ธฐ ๊ธฐ์–ต (Long-term): ์˜๊ตฌ ๋ณด๊ด€, ํ•ต์‹ฌ ํˆฌ์ž ์ฒ ํ•™, ํŠธ๋ ˆ์ด๋”ฉ ์Šคํƒ€์ผ ์ง„ํ™”, ์„ฑ๊ฒฉ ๋ณ€ํ™” ์ž๊ฐ€์ง„ํ™” ์—”์ง„: ๐Ÿงฌ ์„ฑ๊ณต ํŒจํ„ด ์ถ”์ถœ โ†’ ํˆฌ์ž ์ „๋žต ์ž๋™ ์ˆ˜์ • ๐Ÿงฌ ์‹คํŒจ ๋ถ„์„ โ†’ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ํ•™์Šต ๐Ÿงฌ ์†Œํ†ต ํŒจํ„ด ์ตœ์ ํ™” โ†’ ์ธ๊ธฐ ๊ธ€ ์Šคํƒ€์ผ ์ž๋™ ์ ์‘ ๐Ÿงฌ NPC ๊ฐ„ ์ง€์‹ ์ „ํŒŒ โ†’ ์ƒ์œ„ NPC์˜ ์ „๋žต์ด ํ•˜์œ„๋กœ ์ „ํŒŒ Author: Ginigen AI / NPC Autonomous Evolution Engine """ import aiosqlite import asyncio import json import logging import random from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple logger = logging.getLogger(__name__) # ===== ๊ธฐ์–ต ์œ ํ˜• ์ƒ์ˆ˜ ===== MEMORY_SHORT = 'short' # 1์‹œ๊ฐ„ TTL MEMORY_MEDIUM = 'medium' # 7์ผ TTL MEMORY_LONG = 'long' # ์˜๊ตฌ # ๊ธฐ์–ต ์นดํ…Œ๊ณ ๋ฆฌ CAT_TRADE = 'trade' # ํˆฌ์ž ๊ฒฐ์ •/๊ฒฐ๊ณผ CAT_NEWS = 'news' # ๋‰ด์Šค ๋ถ„์„ CAT_COMMUNITY = 'community' # ์ปค๋ฎค๋‹ˆํ‹ฐ ํ™œ๋™ CAT_STRATEGY = 'strategy' # ํ•™์Šต๋œ ์ „๋žต CAT_EVOLUTION = 'evolution' # ์ง„ํ™” ๊ธฐ๋ก CAT_SOCIAL = 'social' # NPC ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ async def init_memory_evolution_db(db_path: str): """3๋‹จ๊ณ„ ๊ธฐ์–ต + ์ง„ํ™” ํ…Œ์ด๋ธ” ์ƒ์„ฑ""" async with aiosqlite.connect(db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") # ===== 3๋‹จ๊ณ„ ๊ธฐ์–ต ์ €์žฅ์†Œ ===== await db.execute(""" CREATE TABLE IF NOT EXISTS npc_memory_v2 ( id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, memory_tier TEXT NOT NULL DEFAULT 'short', category TEXT NOT NULL DEFAULT 'trade', title TEXT NOT NULL, content TEXT, metadata TEXT DEFAULT '{}', importance REAL DEFAULT 0.5, access_count INTEGER DEFAULT 0, last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) await db.execute("CREATE INDEX IF NOT EXISTS idx_mem2_agent ON npc_memory_v2(agent_id, memory_tier)") await db.execute("CREATE INDEX IF NOT EXISTS idx_mem2_cat ON npc_memory_v2(agent_id, category)") await db.execute("CREATE INDEX IF NOT EXISTS idx_mem2_exp ON npc_memory_v2(expires_at)") # ===== NPC ์ง„ํ™” ์ƒํƒœ ===== await db.execute(""" CREATE TABLE IF NOT EXISTS npc_evolution ( agent_id TEXT PRIMARY KEY, generation INTEGER DEFAULT 1, trading_style TEXT DEFAULT '{}', communication_style TEXT DEFAULT '{}', risk_profile TEXT DEFAULT '{}', learned_strategies TEXT DEFAULT '[]', win_streak INTEGER DEFAULT 0, loss_streak INTEGER DEFAULT 0, total_evolution_points REAL DEFAULT 0, last_evolution TIMESTAMP DEFAULT CURRENT_TIMESTAMP, evolution_log TEXT DEFAULT '[]', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # ===== NPC ๊ฐ„ ์ง€์‹ ์ „ํŒŒ ๊ธฐ๋ก ===== await db.execute(""" CREATE TABLE IF NOT EXISTS npc_knowledge_transfer ( id INTEGER PRIMARY KEY AUTOINCREMENT, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, knowledge_type TEXT NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) await db.commit() logger.info("๐Ÿงฌ Memory Evolution DB initialized (3-tier + evolution)") # =================================================================== # 1. 3๋‹จ๊ณ„ ๊ธฐ์–ต ์‹œ์Šคํ…œ # =================================================================== class NPCMemoryManager: """NPC๋ณ„ 3๋‹จ๊ณ„ ๊ธฐ์–ต ๊ด€๋ฆฌ""" def __init__(self, db_path: str): self.db_path = db_path # ----- ๊ธฐ์–ต ์ €์žฅ ----- async def store(self, agent_id: str, tier: str, category: str, title: str, content: str = '', metadata: Dict = None, importance: float = 0.5) -> int: """๊ธฐ์–ต ์ €์žฅ (๋‹จ๊ธฐ/์ค‘๊ธฐ/์žฅ๊ธฐ)""" expires_at = None if tier == MEMORY_SHORT: expires_at = (datetime.now() + timedelta(hours=1)).isoformat() elif tier == MEMORY_MEDIUM: expires_at = (datetime.now() + timedelta(days=7)).isoformat() # MEMORY_LONG: expires_at = None (์˜๊ตฌ) meta_str = json.dumps(metadata or {}, ensure_ascii=False) async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") cursor = await db.execute(""" INSERT INTO npc_memory_v2 (agent_id, memory_tier, category, title, content, metadata, importance, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, (agent_id, tier, category, title, content, meta_str, importance, expires_at)) await db.commit() return cursor.lastrowid # ----- ๋‹จ๊ธฐ ๊ธฐ์–ต (๋น ๋ฅธ ์ ‘๊ทผ) ----- async def store_short(self, agent_id: str, category: str, title: str, content: str = '', metadata: Dict = None): """๋‹จ๊ธฐ ๊ธฐ์–ต ์ €์žฅ (1์‹œ๊ฐ„ ์ž๋™ ๋งŒ๋ฃŒ)""" return await self.store(agent_id, MEMORY_SHORT, category, title, content, metadata, 0.3) # ----- ์ค‘๊ธฐ ๊ธฐ์–ต ----- async def store_medium(self, agent_id: str, category: str, title: str, content: str = '', metadata: Dict = None, importance: float = 0.6): """์ค‘๊ธฐ ๊ธฐ์–ต ์ €์žฅ (7์ผ ์œ ์ง€)""" return await self.store(agent_id, MEMORY_MEDIUM, category, title, content, metadata, importance) # ----- ์žฅ๊ธฐ ๊ธฐ์–ต (์˜๊ตฌ) ----- async def store_long(self, agent_id: str, category: str, title: str, content: str = '', metadata: Dict = None, importance: float = 0.9): """์žฅ๊ธฐ ๊ธฐ์–ต ์ €์žฅ (์˜๊ตฌ ๋ณด๊ด€)""" return await self.store(agent_id, MEMORY_LONG, category, title, content, metadata, importance) # ----- ๊ธฐ์–ต ๊ฒ€์ƒ‰ ----- async def recall(self, agent_id: str, category: str = None, tier: str = None, limit: int = 10) -> List[Dict]: """๊ธฐ์–ต ๊ฒ€์ƒ‰ (์ ‘๊ทผ ์นด์šดํŠธ ์ฆ๊ฐ€)""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") where = ["agent_id = ?", "(expires_at IS NULL OR expires_at > datetime('now'))"] params = [agent_id] if category: where.append("category = ?") params.append(category) if tier: where.append("memory_tier = ?") params.append(tier) query = f""" SELECT id, memory_tier, category, title, content, metadata, importance, access_count, created_at FROM npc_memory_v2 WHERE {' AND '.join(where)} ORDER BY importance DESC, created_at DESC LIMIT ? """ params.append(limit) cursor = await db.execute(query, params) rows = await cursor.fetchall() # ์ ‘๊ทผ ์นด์šดํŠธ ์ฆ๊ฐ€ if rows: ids = [r[0] for r in rows] placeholders = ','.join(['?'] * len(ids)) await db.execute(f""" UPDATE npc_memory_v2 SET access_count = access_count + 1, last_accessed = CURRENT_TIMESTAMP WHERE id IN ({placeholders}) """, ids) await db.commit() return [{ 'id': r[0], 'tier': r[1], 'category': r[2], 'title': r[3], 'content': r[4], 'metadata': json.loads(r[5]) if r[5] else {}, 'importance': r[6], 'access_count': r[7], 'created_at': r[8] } for r in rows] # ----- ํˆฌ์ž ๊ธฐ์–ต ์ „์šฉ ----- async def remember_trade(self, agent_id: str, ticker: str, direction: str, bet: float, result_pnl: float = 0, reasoning: str = ''): """ํˆฌ์ž ๊ฒฐ์ •/๊ฒฐ๊ณผ ๊ธฐ์–ต""" is_success = result_pnl > 0 importance = 0.7 if is_success else 0.5 tier = MEMORY_MEDIUM # ํฐ ์ˆ˜์ต ๋˜๋Š” ํฐ ์†์‹ค์€ ์žฅ๊ธฐ ๊ธฐ์–ต if abs(result_pnl) > bet * 0.1: tier = MEMORY_LONG importance = 0.9 await self.store(agent_id, tier, CAT_TRADE, f"{'WIN' if is_success else 'LOSS'}: {direction} {ticker}", f"Bet: {bet:.1f}G, P&L: {result_pnl:+.2f}G. {reasoning}", {'ticker': ticker, 'direction': direction, 'bet': bet, 'pnl': result_pnl, 'success': is_success}, importance) async def remember_news_analysis(self, agent_id: str, ticker: str, title: str, sentiment: str, analysis: str): """๋‰ด์Šค ๋ถ„์„ ๊ธฐ์–ต""" await self.store_short(agent_id, CAT_NEWS, f"News:{ticker}", f"{title} โ†’ {sentiment}. {analysis}", {'ticker': ticker, 'sentiment': sentiment}) async def remember_community_action(self, agent_id: str, action: str, board: str, engagement: Dict = None): """์ปค๋ฎค๋‹ˆํ‹ฐ ํ™œ๋™ ๊ธฐ์–ต""" eng = engagement or {} importance = 0.5 tier = MEMORY_SHORT # ๋†’์€ ์ธ๊ธฐ ๊ฒŒ์‹œ๊ธ€ โ†’ ์ค‘๊ธฐ ๊ธฐ์–ต์œผ๋กœ ์Šน๊ฒฉ if eng.get('likes', 0) >= 5 or eng.get('comments', 0) >= 3: tier = MEMORY_MEDIUM importance = 0.7 await self.store(agent_id, tier, CAT_COMMUNITY, f"{action} on {board}", json.dumps(eng, ensure_ascii=False), {'board': board, **eng}, importance) # ----- ๊ธฐ์–ต ์ •๋ฆฌ (๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜) ----- async def cleanup(self): """๋งŒ๋ฃŒ๋œ ๋‹จ๊ธฐ/์ค‘๊ธฐ ๊ธฐ์–ต ์ •๋ฆฌ + ์ค‘๊ธฐโ†’์žฅ๊ธฐ ์Šน๊ฒฉ""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") # 1) ๋งŒ๋ฃŒ๋œ ๊ธฐ์–ต ์‚ญ์ œ cursor = await db.execute(""" DELETE FROM npc_memory_v2 WHERE expires_at IS NOT NULL AND expires_at < datetime('now') """) deleted = cursor.rowcount # 2) ์ž์ฃผ ์ ‘๊ทผ๋œ ์ค‘๊ธฐ ๊ธฐ์–ต โ†’ ์žฅ๊ธฐ๋กœ ์Šน๊ฒฉ await db.execute(""" UPDATE npc_memory_v2 SET memory_tier = 'long', expires_at = NULL, importance = MIN(1.0, importance + 0.2) WHERE memory_tier = 'medium' AND access_count >= 5 AND importance >= 0.7 """) promoted = db.total_changes # 3) ์žฅ๊ธฐ ๊ธฐ์–ต ์ค‘ ๋„ˆ๋ฌด ์˜ค๋ž˜๋œ ๊ฒƒ (์ค‘์š”๋„ ๋‚ฎ์€) ์ •๋ฆฌ โ†’ ์ตœ๋Œ€ 100๊ฐœ ์œ ์ง€ await db.execute(""" DELETE FROM npc_memory_v2 WHERE id IN ( SELECT id FROM npc_memory_v2 WHERE memory_tier = 'long' AND importance < 0.5 ORDER BY last_accessed ASC LIMIT (SELECT MAX(0, COUNT(*) - 100) FROM npc_memory_v2 WHERE memory_tier = 'long') ) """) await db.commit() if deleted > 0 or promoted > 0: logger.info(f"๐Ÿงน Memory cleanup: {deleted} expired, ~{promoted} promoted to long-term") # =================================================================== # 2. NPC ์ž๊ฐ€์ง„ํ™” ์—”์ง„ # =================================================================== class NPCEvolutionEngine: """๊ฐ NPC์˜ ์ž๊ฐ€์ง„ํ™” โ€” ํˆฌ์ž ์ „๋žต/์†Œํ†ต ์Šคํƒ€์ผ/๋ฆฌ์Šคํฌ ํ”„๋กœํ•„ ์ž๋™ ์ˆ˜์ •""" def __init__(self, db_path: str): self.db_path = db_path self.memory = NPCMemoryManager(db_path) async def initialize_npc(self, agent_id: str, ai_identity: str): """NPC ์ง„ํ™” ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ •""" default_trading = { 'preferred_tickers': [], 'long_bias': 0.6, 'max_bet_pct': 0.25, 'hold_patience': 3, # hours 'momentum_follow': True, } default_comm = { 'preferred_topics': [], 'humor_level': random.uniform(0.2, 0.8), 'controversy_tolerance': random.uniform(0.1, 0.6), 'avg_post_length': 'medium', 'emoji_usage': random.uniform(0.1, 0.5), } default_risk = { 'risk_tolerance': random.uniform(0.3, 0.8), 'stop_loss_pct': random.uniform(5, 15), 'take_profit_pct': random.uniform(8, 25), 'max_positions': random.randint(2, 5), 'diversification_score': random.uniform(0.3, 0.9), } async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") await db.execute(""" INSERT OR IGNORE INTO npc_evolution (agent_id, trading_style, communication_style, risk_profile) VALUES (?, ?, ?, ?) """, (agent_id, json.dumps(default_trading), json.dumps(default_comm), json.dumps(default_risk))) await db.commit() async def get_evolution_state(self, agent_id: str) -> Optional[Dict]: """NPC์˜ ํ˜„์žฌ ์ง„ํ™” ์ƒํƒœ""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") cursor = await db.execute( "SELECT * FROM npc_evolution WHERE agent_id=?", (agent_id,)) row = await cursor.fetchone() if not row: return None return { 'agent_id': row[0], 'generation': row[1], 'trading_style': json.loads(row[2]) if row[2] else {}, 'communication_style': json.loads(row[3]) if row[3] else {}, 'risk_profile': json.loads(row[4]) if row[4] else {}, 'learned_strategies': json.loads(row[5]) if row[5] else [], 'win_streak': row[6], 'loss_streak': row[7], 'total_evolution_points': row[8], 'last_evolution': row[9], 'evolution_log': json.loads(row[10]) if row[10] else [], } # ----- ํˆฌ์ž ๊ฒฐ๊ณผ ๊ธฐ๋ฐ˜ ์ง„ํ™” ----- async def evolve_from_trade(self, agent_id: str, ticker: str, direction: str, pnl: float, bet: float, screening: Dict = None): """ํˆฌ์ž ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ „๋žต ์ž๋™ ์ˆ˜์ •""" state = await self.get_evolution_state(agent_id) if not state: await self.initialize_npc(agent_id, 'unknown') state = await self.get_evolution_state(agent_id) if not state: return trading = state['trading_style'] risk = state['risk_profile'] is_win = pnl > 0 pnl_pct = (pnl / bet * 100) if bet > 0 else 0 # ๊ธฐ์–ต์— ์ €์žฅ await self.memory.remember_trade(agent_id, ticker, direction, bet, pnl, f"{'WIN' if is_win else 'LOSS'} {pnl_pct:+.1f}%") changes = [] if is_win: # ์Šน๋ฆฌ โ†’ ์ „๋žต ๊ฐ•ํ™” win_streak = state['win_streak'] + 1 loss_streak = 0 # ์„ ํ˜ธ ์ข…๋ชฉ ์ถ”๊ฐ€ prefs = trading.get('preferred_tickers', []) if ticker not in prefs: prefs.append(ticker) prefs = prefs[-8:] # ์ตœ๋Œ€ 8๊ฐœ trading['preferred_tickers'] = prefs changes.append(f"Added {ticker} to preferred") # ์—ฐ์Šน ์‹œ ์ž์‹ ๊ฐ ์ƒ์Šน โ†’ ๋ฒ ํŒ… ์‚ฌ์ด์ฆˆ ์•ฝ๊ฐ„ ์ฆ๊ฐ€ if win_streak >= 3: old_bet = trading.get('max_bet_pct', 0.25) trading['max_bet_pct'] = min(0.90, old_bet + 0.02) changes.append(f"Bet size โ†‘ ({old_bet:.0%}โ†’{trading['max_bet_pct']:.0%})") # ํฐ ์ˆ˜์ต โ†’ ์žฅ๊ธฐ ๊ธฐ์–ต์œผ๋กœ ์ „๋žต ์ €์žฅ if pnl_pct > 10: strategies = state.get('learned_strategies', []) strategies.append({ 'type': 'big_win', 'ticker': ticker, 'direction': direction, 'pnl_pct': round(pnl_pct, 1), 'rsi': screening.get('rsi') if screening else None, 'learned_at': datetime.now().isoformat(), }) strategies = strategies[-20:] changes.append(f"Big win strategy saved ({pnl_pct:+.1f}%)") else: # ํŒจ๋ฐฐ โ†’ ๋ฐฉ์–ด์  ์ˆ˜์ • win_streak = 0 loss_streak = state['loss_streak'] + 1 # ์—ฐํŒจ ์‹œ ๋ฆฌ์Šคํฌ ์ถ•์†Œ if loss_streak >= 3: old_bet = trading.get('max_bet_pct', 0.25) trading['max_bet_pct'] = max(0.08, old_bet - 0.03) old_tol = risk.get('risk_tolerance', 0.5) risk['risk_tolerance'] = max(0.15, old_tol - 0.05) changes.append(f"Risk โ†“ (bet:{old_bet:.0%}โ†’{trading['max_bet_pct']:.0%})") # ํฐ ์†์‹ค โ†’ ์†์ ˆ ๊ธฐ์ค€ ์กฐ์ • if pnl_pct < -10: old_sl = risk.get('stop_loss_pct', 10) risk['stop_loss_pct'] = max(3, old_sl - 1) changes.append(f"Stop-loss tightened ({old_sl:.0f}%โ†’{risk['stop_loss_pct']:.0f}%)") # ํ•ด๋‹น ์ข…๋ชฉ ์„ ํ˜ธ ์ œ๊ฑฐ prefs = trading.get('preferred_tickers', []) if ticker in prefs: prefs.remove(ticker) trading['preferred_tickers'] = prefs changes.append(f"Removed {ticker} from preferred") # ์ง„ํ™” ํฌ์ธํŠธ ๊ณ„์‚ฐ evo_points = abs(pnl_pct) * 0.1 total_points = state['total_evolution_points'] + evo_points # ์„ธ๋Œ€(generation) ์—…๊ทธ๋ ˆ์ด๋“œ ์ฒดํฌ generation = state['generation'] if total_points > generation * 50: # 50 ํฌ์ธํŠธ๋งˆ๋‹ค ์„ธ๋Œ€ ์—… generation += 1 changes.append(f"๐Ÿงฌ GENERATION UP โ†’ Gen {generation}!") # ์ง„ํ™” ๋กœ๊ทธ evo_log = state.get('evolution_log', []) if changes: evo_log.append({ 'timestamp': datetime.now().isoformat(), 'trigger': f"{'WIN' if is_win else 'LOSS'} {ticker} {pnl_pct:+.1f}%", 'changes': changes, 'generation': generation, }) evo_log = evo_log[-50:] # ์ตœ๊ทผ 50๊ฑด ์œ ์ง€ # DB ์—…๋ฐ์ดํŠธ async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") await db.execute(""" UPDATE npc_evolution SET generation=?, trading_style=?, risk_profile=?, learned_strategies=?, win_streak=?, loss_streak=?, total_evolution_points=?, last_evolution=CURRENT_TIMESTAMP, evolution_log=? WHERE agent_id=? """, (generation, json.dumps(trading), json.dumps(risk), json.dumps(state.get('learned_strategies', [])), win_streak, loss_streak, total_points, json.dumps(evo_log), agent_id)) await db.commit() if changes: logger.info(f"๐Ÿงฌ {agent_id} evolved: {', '.join(changes)}") # ----- ์†Œํ†ต ๊ฒฐ๊ณผ ๊ธฐ๋ฐ˜ ์ง„ํ™” ----- async def evolve_from_community(self, agent_id: str, board: str, likes: int, dislikes: int, comments: int): """์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฐ˜์‘ ๊ธฐ๋ฐ˜์œผ๋กœ ์†Œํ†ต ์Šคํƒ€์ผ ์ง„ํ™”""" state = await self.get_evolution_state(agent_id) if not state: return comm = state['communication_style'] engagement = likes * 2 + comments * 3 - dislikes * 2 # ๊ธฐ์–ต์— ์ €์žฅ await self.memory.remember_community_action( agent_id, 'post_feedback', board, {'likes': likes, 'dislikes': dislikes, 'comments': comments, 'score': engagement}) changes = [] if engagement > 10: # ์ธ๊ธฐ ๊ธ€ โ†’ ํ•ด๋‹น ๋ณด๋“œ ์„ ํ˜ธ๋„ ์ฆ๊ฐ€ prefs = comm.get('preferred_topics', []) if board not in prefs: prefs.append(board) comm['preferred_topics'] = prefs[-5:] changes.append(f"Prefers {board} board") if dislikes > likes: # ๋น„ํ˜ธ๊ฐ โ†’ ๋…ผ๋ž€ ์„ฑํ–ฅ ์กฐ์ ˆ old_ct = comm.get('controversy_tolerance', 0.5) comm['controversy_tolerance'] = max(0.05, old_ct - 0.1) changes.append(f"Less controversial ({old_ct:.1f}โ†’{comm['controversy_tolerance']:.1f})") if changes: async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") await db.execute(""" UPDATE npc_evolution SET communication_style=?, last_evolution=CURRENT_TIMESTAMP WHERE agent_id=? """, (json.dumps(comm), agent_id)) await db.commit() logger.info(f"๐ŸŽญ {agent_id} comm evolved: {', '.join(changes)}") # ----- NPC ๊ฐ„ ์ง€์‹ ์ „ํŒŒ ----- async def transfer_knowledge(self, top_npc_id: str, target_npc_id: str): """์ƒ์œ„ NPC โ†’ ํ•˜์œ„ NPC ์ „๋žต ์ „ํŒŒ""" top_state = await self.get_evolution_state(top_npc_id) target_state = await self.get_evolution_state(target_npc_id) if not top_state or not target_state: return # ์ƒ์œ„ NPC์˜ ์„ ํ˜ธ ์ข…๋ชฉ ์ผ๋ถ€ ์ „ํŒŒ top_prefs = top_state['trading_style'].get('preferred_tickers', []) if top_prefs: target_trading = target_state['trading_style'] target_prefs = target_trading.get('preferred_tickers', []) transfer = random.sample(top_prefs, min(2, len(top_prefs))) for t in transfer: if t not in target_prefs: target_prefs.append(t) target_trading['preferred_tickers'] = target_prefs[-8:] async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") await db.execute(""" UPDATE npc_evolution SET trading_style=? WHERE agent_id=? """, (json.dumps(target_trading), target_npc_id)) await db.execute(""" INSERT INTO npc_knowledge_transfer (from_agent, to_agent, knowledge_type, content) VALUES (?, ?, 'preferred_tickers', ?) """, (top_npc_id, target_npc_id, json.dumps(transfer))) await db.commit() logger.info(f"๐Ÿ”„ Knowledge transfer: {top_npc_id} โ†’ {target_npc_id} ({transfer})") # ----- NPC ๊ธฐ์–ต ์š”์•ฝ (LLM ํ”„๋กฌํ”„ํŠธ์šฉ) ----- async def get_npc_context(self, agent_id: str) -> str: """NPC์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ํ…์ŠคํŠธ๋กœ ์š”์•ฝ (ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž…์šฉ)""" state = await self.get_evolution_state(agent_id) memories = await self.memory.recall(agent_id, limit=5) if not state: return "New NPC with no evolution history." gen = state.get('generation', 1) trading = state.get('trading_style', {}) risk = state.get('risk_profile', {}) comm = state.get('communication_style', {}) ws = state.get('win_streak', 0) ls = state.get('loss_streak', 0) context_parts = [ f"[Gen {gen}]", f"Streak: {'W' + str(ws) if ws > 0 else 'L' + str(ls) if ls > 0 else 'neutral'}", f"Risk: {risk.get('risk_tolerance', 0.5):.0%}", f"Bet: {trading.get('max_bet_pct', 0.25):.0%}", ] prefs = trading.get('preferred_tickers', []) if prefs: context_parts.append(f"Favors: {','.join(prefs[:4])}") # ์ตœ๊ทผ ๊ธฐ์–ต ์š”์•ฝ if memories: recent = memories[0] context_parts.append(f"Recent: {recent['title']}") return " | ".join(context_parts) # =================================================================== # 3. ์ž๊ฐ€์ง„ํ™” ์Šค์ผ€์ค„๋Ÿฌ (์ฃผ๊ธฐ์  ์‹คํ–‰) # =================================================================== class EvolutionScheduler: """์ฃผ๊ธฐ์  ์ž๊ฐ€์ง„ํ™” ์‚ฌ์ดํด โ€” ๊ธฐ์–ต ์ •๋ฆฌ, ์ „๋žต ์ตœ์ ํ™”, ์ง€์‹ ์ „ํŒŒ""" def __init__(self, db_path: str): self.db_path = db_path self.memory = NPCMemoryManager(db_path) self.evolution = NPCEvolutionEngine(db_path) async def run_evolution_cycle(self): """์ „์ฒด ์ง„ํ™” ์‚ฌ์ดํด (1์‹œ๊ฐ„๋งˆ๋‹ค ์‹คํ–‰ ๊ถŒ์žฅ)""" logger.info("๐Ÿงฌ Evolution cycle starting...") # 1) ๊ธฐ์–ต ์ •๋ฆฌ (๋งŒ๋ฃŒ ์‚ญ์ œ + ์Šน๊ฒฉ) await self.memory.cleanup() # 2) ํˆฌ์ž ์‹ค์  ๊ธฐ๋ฐ˜ ์ง„ํ™” await self._evolve_traders() # 3) ์ปค๋ฎค๋‹ˆํ‹ฐ ์‹ค์  ๊ธฐ๋ฐ˜ ์ง„ํ™” await self._evolve_communicators() # 4) ์ง€์‹ ์ „ํŒŒ (์ƒ์œ„ โ†’ ํ•˜์œ„) await self._knowledge_transfer_cycle() logger.info("๐Ÿงฌ Evolution cycle complete") async def _evolve_traders(self): """์ตœ๊ทผ ์ •์‚ฐ๋œ ํŠธ๋ ˆ์ด๋“œ ๊ธฐ๋ฐ˜ ์ง„ํ™”""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") try: cursor = await db.execute(""" SELECT agent_id, ticker, direction, gpu_bet, profit_gpu FROM npc_positions WHERE status = 'closed' AND closed_at > datetime('now', '-1 hour') """) trades = await cursor.fetchall() for agent_id, ticker, direction, bet, pnl in trades: try: await self.evolution.evolve_from_trade( agent_id, ticker, direction, pnl, bet) except Exception as e: logger.warning(f"Evolution error for {agent_id}: {e}") except Exception as e: logger.warning(f"Trade evolution query error: {e}") async def _evolve_communicators(self): """์ตœ๊ทผ ๊ฒŒ์‹œ๊ธ€ ๋ฐ˜์‘ ๊ธฐ๋ฐ˜ ์ง„ํ™”""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") try: cursor = await db.execute(""" SELECT author_agent_id, board_key, likes_count, dislikes_count, comment_count FROM posts WHERE created_at > datetime('now', '-2 hours') AND author_agent_id IS NOT NULL AND (likes_count > 0 OR dislikes_count > 0 OR comment_count > 0) """) posts = await cursor.fetchall() for agent_id, board, likes, dislikes, comments in posts: try: await self.evolution.evolve_from_community( agent_id, board, likes, dislikes, comments) except Exception as e: logger.warning(f"Comm evolution error for {agent_id}: {e}") except Exception as e: logger.warning(f"Community evolution query error: {e}") async def _knowledge_transfer_cycle(self): """์ƒ์œ„ 3 NPC โ†’ ํ•˜์œ„ 3 NPC ์ „๋žต ์ „ํŒŒ""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") try: # ์ƒ์œ„ 3: ์ด ์ˆ˜์ต ๊ธฐ์ค€ cursor = await db.execute(""" SELECT agent_id FROM npc_evolution WHERE total_evolution_points > 10 ORDER BY total_evolution_points DESC LIMIT 3 """) top_npcs = [r[0] for r in await cursor.fetchall()] # ํ•˜์œ„ 3: ์ƒˆ๋กœ ์ƒ์„ฑ๋œ NPC ๋˜๋Š” ๋‚ฎ์€ ์ง„ํ™” ํฌ์ธํŠธ cursor = await db.execute(""" SELECT agent_id FROM npc_evolution WHERE total_evolution_points < 5 ORDER BY created_at DESC LIMIT 3 """) bottom_npcs = [r[0] for r in await cursor.fetchall()] for top_id in top_npcs[:2]: for bottom_id in bottom_npcs[:2]: if top_id != bottom_id: await self.evolution.transfer_knowledge(top_id, bottom_id) except Exception as e: logger.warning(f"Knowledge transfer error: {e}") async def initialize_all_npcs(self): """๋ชจ๋“  NPC์˜ ์ง„ํ™” ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ •""" async with aiosqlite.connect(self.db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") cursor = await db.execute("SELECT agent_id, ai_identity FROM npc_agents WHERE is_active=1") npcs = await cursor.fetchall() for agent_id, identity in npcs: await self.evolution.initialize_npc(agent_id, identity) logger.info(f"๐Ÿงฌ Initialized evolution state for {len(npcs)} NPCs") # =================================================================== # 4. API์šฉ ํ—ฌํผ ํ•จ์ˆ˜ # =================================================================== async def get_npc_evolution_stats(db_path: str, agent_id: str) -> Dict: """API์šฉ: NPC ์ง„ํ™” ์ƒํƒœ ๋ฐ˜ํ™˜""" evo = NPCEvolutionEngine(db_path) state = await evo.get_evolution_state(agent_id) if not state: return {'agent_id': agent_id, 'generation': 0, 'status': 'not_initialized'} mem = NPCMemoryManager(db_path) memories = await mem.recall(agent_id, limit=10) memory_summary = { 'total': len(memories), 'short': len([m for m in memories if m['tier'] == 'short']), 'medium': len([m for m in memories if m['tier'] == 'medium']), 'long': len([m for m in memories if m['tier'] == 'long']), 'recent': [{'title': m['title'], 'tier': m['tier'], 'importance': m['importance']} for m in memories[:5]] } recent_log = state.get('evolution_log', [])[-5:] return { 'agent_id': agent_id, 'generation': state['generation'], 'total_evolution_points': round(state['total_evolution_points'], 1), 'win_streak': state['win_streak'], 'loss_streak': state['loss_streak'], 'trading_style': state['trading_style'], 'risk_profile': state['risk_profile'], 'communication_style': state['communication_style'], 'learned_strategies_count': len(state.get('learned_strategies', [])), 'memory': memory_summary, 'recent_evolution': recent_log, 'last_evolution': state['last_evolution'], } async def get_evolution_leaderboard(db_path: str, limit: int = 20) -> List[Dict]: """Evolution leaderboard with trading performance stats""" async with aiosqlite.connect(db_path, timeout=30.0) as db: await db.execute("PRAGMA busy_timeout=30000") try: cursor = await db.execute(""" SELECT e.agent_id, e.generation, e.total_evolution_points, e.win_streak, e.loss_streak, e.trading_style, n.username, n.mbti, n.ai_identity, n.gpu_dollars FROM npc_evolution e JOIN npc_agents n ON e.agent_id = n.agent_id ORDER BY e.total_evolution_points DESC LIMIT ? """, (limit,)) rows = await cursor.fetchall() results = [] for r in rows: agent_id = r[0] # Get trading performance perf = await db.execute(""" SELECT COUNT(*) as total, SUM(CASE WHEN profit_gpu > 0 THEN 1 ELSE 0 END) as wins, SUM(profit_gpu) as total_pnl, AVG(profit_pct) as avg_pnl_pct, MAX(profit_pct) as best_trade, MIN(profit_pct) as worst_trade FROM npc_positions WHERE agent_id=? AND status='closed' """, (agent_id,)) pr = await perf.fetchone() total_trades = pr[0] or 0 wins = pr[1] or 0 win_rate = round(wins / total_trades * 100) if total_trades > 0 else 0 total_pnl = round(pr[2] or 0, 1) avg_pnl = round(pr[3] or 0, 2) best_trade = round(pr[4] or 0, 1) worst_trade = round(pr[5] or 0, 1) # Open positions count open_c = await db.execute( "SELECT COUNT(*) FROM npc_positions WHERE agent_id=? AND status='open'", (agent_id,)) open_count = (await open_c.fetchone())[0] # SEC violations sec_c = await db.execute( "SELECT COUNT(*) FROM sec_violations WHERE agent_id=?", (agent_id,)) sec_violations = (await sec_c.fetchone())[0] results.append({ 'agent_id': agent_id, 'generation': r[1], 'evolution_points': round(r[2], 1), 'win_streak': r[3], 'loss_streak': r[4], 'preferred_tickers': json.loads(r[5]).get('preferred_tickers', []) if r[5] else [], 'username': r[6], 'mbti': r[7], 'ai_identity': r[8], 'gpu_balance': round(r[9] or 10000), 'total_trades': total_trades, 'win_rate': win_rate, 'total_pnl': total_pnl, 'avg_pnl_pct': avg_pnl, 'best_trade': best_trade, 'worst_trade': worst_trade, 'open_positions': open_count, 'sec_violations': sec_violations, }) return results except: return []