humanda5
NPC ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์™„์„ฑ
a147abc
# portfolio/npc_social_network/npc/npc_base.py
import random
import copy
import re
from typing import Optional, List
from datetime import datetime
from .npc_memory import Memory, MemoryStore
from .npc_emotion import EmotionManager
from .npc_behavior import BehaviorManager
from .emotion_config import (EMOTION_LIST, EMOTION_CATEGORY_MAP, EMOTION_DECAY_RATE
, PERSONALITY_TEMPLATE, EMOTION_RELATION_IMPACT
, POSITIVE_RELATION_EMOTIONS, NEGATIVE_RELATION_EMOTIONS
, COGNITIVE_RELATION_EMOTIONS)
from .personality_config import AGE_PROFILE, PERSONALITY_PROFILE
from .npc_relationship import RelationshipManager
from ..models.llm_helper import (query_llm_with_prompt, query_llm_for_emotion, summarize_memories
, analyze_gossip, classify_and_extract_plan_details
, evaluate_social_action_outcome
, generate_reflection_topic)
from .npc_memory_embedder import search_similar_memories, add_memory_to_index
from .npc_manager import get_korean_postposition
from .npc_planner import PlannerManager
from .. import simulation_core
from typing import TYPE_CHECKING, Tuple, List
if TYPE_CHECKING:
from .npc_manager import NPCManager
from ..manager.conversation_manager import Conversation
# Personality ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์š”์•ฝ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํด๋ž˜์Šค ์ •์˜
class PersonalityManager:
"""
NPC์˜ ์„ฑ๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํด๋ž˜์Šค
"""
def __init__(self, initial_traits=None):
self.traits = copy.deepcopy(initial_traits) if initial_traits else copy.deepcopy(PERSONALITY_TEMPLATE)
def get(self, key, default=None):
return self.traits.get(key, default)
def __getitem__(self, key):
return self.traits[key]
def __setitem__(self, key, value):
self.traits[key] = value
def keys(self):
return self.traits.keys()
def get_narrative_summary(self) -> str:
"""ํ˜„์žฌ ์„ฑ๊ฒฉ ์ˆ˜์น˜๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์„œ์ˆ ์ ์ธ ๋ฌ˜์‚ฌ๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑ"""
narratives = []
sensitive = self.traits.get("sensitive", 0.5)
stoic = self.traits.get("stoic", 0.5)
congitive_bias = self.traits.get("cognitive_bias", 0.5)
# ๊ฐ ์„ฑ๊ฒฉ ์ถ•์— ๋Œ€ํ•œ ์„œ์ˆ ์  ๋ฌ˜์‚ฌ ์ƒ์„ฑ
if sensitive > 0.7 and stoic < 0.3:
narratives.append("๊ฐ์ˆ˜์„ฑ์ด ๋งค์šฐ ํ’๋ถ€ํ•˜๊ณ  ๊ฐ์ •์˜ ์˜ํ–ฅ์„ ์‰ฝ๊ฒŒ ๋ฐ›๋Š” ํŽธ์ž…๋‹ˆ๋‹ค.")
elif stoic > 0.7 and sensitive < 0.3:
narratives.append("๊ฐ์ •์„ ๊ฑฐ์˜ ๋“œ๋Ÿฌ๋‚ด์ง€ ์•Š๋Š” ๊ณผ๋ฌตํ•˜๊ณ  ๋ฌด๋˜ํ•œ ์„ฑํ–ฅ์ž…๋‹ˆ๋‹ค.")
else:
narratives.append("์ƒํ™ฉ์— ๋”ฐ๋ผ ๊ฐ์ •์„ ํ‘œํ˜„ํ•  ์ค„ ์•„๋Š” ๊ท ํ˜• ์žกํžŒ ๋ชจ์Šต์„ ๋ณด์ž…๋‹ˆ๋‹ค.")
if congitive_bias > 0.7:
narratives.append("๋Œ€ํ™”๋ฅผ ํ•  ๋•Œ ๋…ผ๋ฆฌ์ ์ด๊ณ  ์‚ฌ์‹ค์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ํŒ๋‹จํ•˜๋ ค๋Š” ๊ฒฝํ–ฅ์ด ๊ฐ•ํ•ฉ๋‹ˆ๋‹ค.")
elif congitive_bias < 0.3:
narratives.append("์ง๊ด€๊ณผ ๊ฐ์ •์— ๋”ฐ๋ผ ์ƒํ™ฉ์„ ํŒ๋‹จํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.")
if not narratives:
return "ํ‰๋ฒ”ํ•˜๊ณ  ๊ท ํ˜• ์žกํžŒ ์„ฑ๊ฒฉ์ž…๋‹ˆ๋‹ค."
return "".join(narratives)
# NPC ํด๋ž˜์Šค ์ •์˜
class NPC:
"""
NPC ํด๋ž˜์Šค
NPC์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ๊ฐ์ฒดํ™”
"""
def __init__(self, name: str, korean_name: str, job: str,
personality: Optional[dict]=None):
self.name = name
self.korean_name = korean_name
self.job = job
self.manager: Optional['NPCManager'] = None
# npc ๊ธฐ์–ต
self.memory_store = MemoryStore()
# ๊ฐ์ • ์ƒํƒœ ๋“ฑ ๊ฐ์ • ๊ด€๋ฆฌ์ž ์ดˆ๊ธฐํ™”
self.personality = PersonalityManager(personality)
self.relationships = RelationshipManager(self)
self.reputation = {} # ๋‹ค๋ฅธ NPC์— ๋Œ€ํ•œ ํ‰ํŒ ์ €์žฅ
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
self.knowledge: dict[str, dict] = {} # ๋‹ค๋ฅธ NPC์— ๋Œ€ํ•œ ์‚ฌ์‹ค ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” '์ง€์‹ ๋ฒ ์ด์Šค'
# ํ–‰๋™ ๊ด€๋ฆฌ์ž
self.behavior = BehaviorManager()
self.planner = PlannerManager(self)
# ๋‚ด๋ถ€: float ๊ฐ์ • ์ˆ˜์น˜ ๊ด€๋ฆฌ์šฉ ๋ฒ„ํผ
self._emotion_buffer = self.emotion.get_buffer()
# baseline ๊ฐ์ • ๊ฐ’ ๋ถ€์—ฌ
baseline_buffer = {}
for emo in self._emotion_buffer:
base_min = 0.05
base_max = 0.3
category = EMOTION_CATEGORY_MAP.get(emo, None)
bias_map = {
"core": "affect_bias", "social": "social_bias",
"cognitive": "cognitive_bias", "complex": "complex_bias"
}
bias = self.personality.get(bias_map.get(category, "sensitive"), 1.0)
baseline_value = base_min + (base_max - base_min) * bias
baseline_value *= self.personality.get("sensitive", 1.0) # ์ „์ฒด ๋ฏผ๊ฐ๋„ ์ ์šฉ
baseline_buffer[emo] = round(baseline_value, 3)
# baseline buffer๋ฅผ EmotionManager์— ๋“ฑ๋ก
self.emotion.set_baseline(baseline_buffer)
# emotion_buffer ์ดˆ๊ธฐํ™”
self._emotion_buffer = self.emotion.get_buffer()
# ํ”„๋กœํ•„ ์ง€์ • โ†’ ๊ธฐ๋ณธ์€ "stable", ๋‚˜์ค‘์— NPC๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ ๋ถ€์—ฌ
profile_type = "stable"
profile = PERSONALITY_PROFILE[profile_type]
# Personality ๋ณ€ํ™” ์†๋„ ๊ฐœ๋ณ„ ์„ค์ •
self.personality_change_rate = {
"sensitive": random.uniform(*profile["sensitive"]),
"stoic": random.uniform(*profile["stoic"]),
"cognitive_bias": random.uniform(*profile["cognitive_bias"]),
}
# Personality baseline ์ €์žฅ (์ดˆ๊ธฐ๊ฐ’ ๋ณต์‚ฌ)
self.personality_baseline = copy.deepcopy(self.personality.traits)
# ๋‚˜์ด ์ถ”๊ฐ€ (18 ~ 50์„ธ ๊ธฐ๋ณธ ๋žœ๋ค ์ดˆ๊ธฐํ™” โ†’ ๋‚˜์ค‘์— ์ƒ์„ฑ ์‹œ age ์ง€์ • ๊ฐ€๋Šฅ)
self.age = random.randint(18, 50)
# ์ดˆ๊ธฐ personality_stage๋Š” update_personality_stage()์—์„œ ์ž๋™ ์„ค์ •
self.personality_stage = None
self.update_personality_stage()
def generate_dialogue_turn(self, conversation: "Conversation", is_final_turn: bool=False) -> Tuple[str, str]:
"""๋Œ€ํ™”์˜ ํ˜„์žฌ ํ„ด์— ๋Œ€ํ•œ ์‘๋‹ต๊ณผ ํ–‰๋™์„ ์ƒ์„ฑ (๊ธฐ์–ต๊ณผ ๊ด€๊ณ„๋ฅผ ์ด ๋™์›)"""
from ..models.llm_helper import generate_dialogue_action
# 1. ์ƒ๊ฐํ•˜๊ธฐ: '์ฒซ ๋งˆ๋””'์™€ '๋Œ€๋‹ต'์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ •๋ณด๋กœ ๊ธฐ์–ต์„ ํƒ์ƒ‰.
if not conversation.conversation_history: # ๋Œ€ํ™” ๊ธฐ๋ก์ด ์—†์œผ๋ฉด '์ฒซ ๋งˆ๋””' ์ฐจ๋ก€
query_text = conversation.topic
else: # ๋Œ€ํ™” ๊ธฐ๋ก์ด ์žˆ์œผ๋ฉด '๋Œ€ํ™”' ์ฐจ๋ก€
query_text = "\n".join(conversation.conversation_history[-2:]) # ์ตœ๊ทผ 2ํ„ด์˜ ๋Œ€ํ™”๋กœ ๊ธฐ์–ต ํƒ์ƒ‰
_, _, relevant_memories = search_similar_memories(self, query_text, top_k=3)
# 2. ๋Œ€์‚ฌ ์ƒ์„ฑ: ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ llm_helper์— ์š”์ฒญ
result = generate_dialogue_action(
speaker = self,
target = conversation.participants[1-conversation.turn_index],
conversation=conversation,
memories = relevant_memories,
is_final_turn = is_final_turn
)
dialogue = result.get("dialogue", "...")
action = result.get("action", "END").strip().upper()
# ํ–‰๋™ํ•˜๊ธฐ (์ž ๊ธˆ๊ณผ ํ•จ๊ป˜)
with simulation_core.simulation_lock:
emotion = query_llm_for_emotion(dialogue)
if emotion and emotion in EMOTION_LIST:
self.update_emotion(emotion, 1.0)
return dialogue, action
def remember(self, content: str, importance: int = 5, emotion: str = None,
strength: float = 1.0, memory_type:str = "Event",
context_tags: Optional[List[str]]=None) -> Memory:
"""
NPC๊ฐ€ ์ƒˆ๋กœ์šด ๊ธฐ์–ต์„ ์ €์žฅํ•˜๊ณ  ๊ฐ์ • ์ƒํƒœ์— ๋ฐ˜์˜
"""
memory = Memory(
content=content,
importance = importance,
emotion=emotion or "neutral",
memory_type=memory_type,
context_tags=context_tags
)
self.memory_store.add_memory(memory)
# ์ค‘์š”๋„๊ฐ€ 3 ์ด์ƒ์ธ ๊ธฐ์–ต๋งŒ ๋ฒกํ„ฐ ์ธ๋ฑ์Šค์— ์ถ”๊ฐ€ํ•˜์—ฌ ํšจ์œจ์„ฑ ํ™•๋ณด
if memory.importance >= 3:
add_memory_to_index(self, memory)
if emotion and emotion in EMOTION_LIST:
self.update_emotion(emotion, strength) # strength = importance / 5.0์œผ๋กœ๋„ ๊ฐ€๋Šฅ
# ๋งŒ์•ฝ ๊ธฐ์–ต์ด '์†Œ๋ฌธ'์ด๋ผ๋ฉด, ํ‰ํŒ์„ ์—…๋ฐ์ดํŠธ
if memory_type == "Gossip":
# (๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ: LLM์œผ๋กœ ์†Œ๋ฌธ์˜ ๋Œ€์ƒ๊ณผ ๊ธ/๋ถ€์ •์„ ํŒŒ์•…ํ•ด์•ผ ๋” ์ •ํ™•ํ•จ)
# ๊ธ์ • ์†Œ๋ฌธ ์˜ˆ์‹œ: "๋ฐฅ์ด ์ฐฐ๋ฆฌ๋ฅผ ๋„์™”๋‹ค"
# ๋ถ€์ • ์†Œ๋ฌธ ์˜ˆ์‹œ: "์—˜๋ฆฐ์ด ์•จ๋ฆฌ์Šค์™€ ๋‹คํ‰œ๋‹ค"
self.process_gossip_and_update_reputation(memory)
return memory # ์ƒ์„ฑ๋œ ๊ธฐ์–ต ๊ฐ์ฒด ๋ฐ˜ํ™˜
def update_emotion(self, emotion:str, strength: float = 1.0):
"""
ํŠน์ • ๊ฐ์ •์„ ๊ฐ์ • ์ƒํƒœ์— ๋ฐ˜์˜
"""
self.emotion.update_emotion(emotion, strength)
self._emotion_buffer = self.emotion.get_buffer()
def decay_emotions(self):
"""
์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ๋ชจ๋“  ๊ฐ์ •์ด ์„œ์„œํžˆ ๊ฐ์†Œ
"""
self.emotion.decay_emotions()
self._emotion_buffer = self.emotion.get_buffer()
def recall_all_memories(self) -> List[str]:
"""
์ €์žฅ๋œ ๋ชจ๋“  ๊ธฐ์–ต(๋‹จ๊ธฐ + ์žฅ๊ธฐ)์„ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜
"""
return [m.content for m in self.memory_store.get_all_memories()]
def get_composite_emotion_state(self, top_n: int = 3) -> List[tuple]:
"""
ํ˜„์žฌ ๊ฐ์ • ์ƒํƒœ ์ค‘ ์ƒ์œ„ N๊ฐœ๋ฅผ ๋ฐ˜ํ™˜
"""
buffer = self.emotion.get_buffer() # EmotionManager์—์„œ ์ตœ์‹  ๊ฐ์ • ์ƒํƒœ ๋ณต์‚ฌ๋ณธ ๊ฐ€์ ธ์˜ค๊ธฐ
sorted_emotions = sorted(buffer.items(), key=lambda x: x[1], reverse=True)
composite = [(emo,round(score, 2)) for emo, score in sorted_emotions[:top_n] if score > 0.1]
return composite # ์˜ˆ [('fear', 2.0), ('anger'), 1.5]
def summarize_emotional_state(self) -> str:
"""
๊ฐ์ • ์ƒํƒœ ์š”์•ฝ ํ…์ŠคํŠธ ์ƒ์„ฑ
"""
composite = self.get_composite_emotion_state()
dominant = self.emotion.get_dominant_emotion()
if not dominant:
return "ํ˜„์žฌ ํŠน๋ณ„ํ•œ ๊ฐ์ • ์—†์ด ํ‰์˜จํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค."
return f"ํ˜„์žฌ {dominant} ๋“ฑ์˜ ๊ฐ์ •์„ ๋А๋ผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.\n(์ฃผ์š” ๊ฐ์ •: {composite if composite else '์—†์Œ'})"
def get_relationship_description(self, target_name: str) -> str:
"""
ํŠน์ • ๋Œ€์ƒ๊ณผ์˜ ๊ด€๊ณ„๋ฅผ ์ž์—ฐ์–ด๋กœ ๋ฐ˜ํ™˜
"""
return self.relationships.get_relationship_summary(target_name)
def get_top_emotions(self, top_n=6):
"""
ํ˜„์žฌ NPC ๊ฐ์ • ์ƒํƒœ ์ƒ์œ„ N๊ฐœ ๋ฐ˜ํ™˜
"""
return self.emotion.get_top_emotions(top_n=top_n)
def update_personality(self):
"""
๊ฒฝํ—˜์— ๋”ฐ๋ผ ์„ฑ๊ฒฉ์„ ์ ์ง„์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ด
- MemoryType ๋ณ„ ๊ฐ€์ค‘์น˜ + EmotionInfluence ๋ณ„ ๊ฐ€์ค‘์น˜ ๋ชจ๋‘ ๋ฐ˜์˜
"""
recent_memories = [m for m in self.memory_store.get_recent_memories(limit=10) if m.importance >= 5]
if not recent_memories:
return
total_influence = {"sensitive": 0.0, "stoic": 0.0, "cognitive_bias": 0.0}
# Personality ๋ณ€ํ™” ๋ฐ˜์˜
for mem in recent_memories:
if not mem.emotion or mem.emotion not in EMOTION_RELATION_IMPACT: continue
# ๊ฐ ๊ฐ์ •์˜ ๊ณ ์œ ํ•œ ์˜ํ–ฅ๋ ฅ(impact)์„ ๊ฐ€์ ธ์˜ด
emotion_impact = EMOTION_RELATION_IMPACT.get(mem.emotion, 0.0)
# 1. ๊ฐ์ •์˜ ๊ธ์ •/๋ถ€์ •/์ธ์ง€์  ์†์„ฑ์— ๋”ฐ๋ฅธ ์˜ํ–ฅ ๋ฐฉํ–ฅ ๊ฒฐ์ •
influence_vector = {"sensitive": 0.0, "stoic": 0.0, "cognitive_bias": 0.0}
# ๊ฐ์ • ์˜ํ–ฅ๋ ฅ์ด ์–‘์ˆ˜์ด๋ฉด(๊ธ์ •์  ๊ฐ์ •), ๋ฏผ๊ฐ๋„์™€ ๋‚ด์„ฑ์„ ๋‚ฎ์ถค
if mem.emotion in POSITIVE_RELATION_EMOTIONS:
influence_vector["sensitive"] -= abs(emotion_impact) / 2.0 # ๊ธ์ •์  ๊ฒฝํ—˜์€ ๋ฏผ๊ฐ๋„๋ฅผ ๋‚ฎ์ถค
influence_vector["stoic"] -= abs(emotion_impact) / 4.0 # ๊ธ์ •์ ์ธ ๊ฒฝํ—˜์€ ๋‚ด์„ฑ์ ์ผ ํ•„์š”๋ฅผ ์ค„์ž„
# ๊ฐ์ • ์˜ํ–ฅ๋ ฅ์ด ์Œ์ˆ˜์ด๋ฉด(๋ถ€์ •์  ๊ฐ์ •), ๋ฏผ๊ฐ๋„์™€ ๋‚ด์„ฑ์„ ๋†’์ž„
elif mem.emotion in NEGATIVE_RELATION_EMOTIONS:
influence_vector["sensitive"] += abs(emotion_impact) / 2.0 # ๋ถ€์ •์  ๊ฒฝํ—˜์€ ๋ฏผ๊ฐ๋„๋ฅผ ๋†’์ž„
influence_vector["stoic"] += abs(emotion_impact) / 4.0 # ๋ถ€์ •์  ๊ฒฝํ—˜์€ ๋‚ด์„ฑ์ ์œผ๋กœ ๋งŒ๋“ฆ
# ์ธ์ง€์  ๊ฐ์ •์€ cognitive_bias์— ์˜ํ–ฅ
if mem.emotion in COGNITIVE_RELATION_EMOTIONS:
influence_vector["cognitive_bias"] += 0.4 # ์ธ์ง€์  ๊ฒฝํ—˜์€ ๋…ผ๋ฆฌ์  ์‚ฌ๊ณ ๋ฅผ ๊ฐ•ํ™”
# 2. ๋ณ€ํ™” ๊ฐ•๋„ ๊ณ„์‚ฐ (๊ธฐ์–ต์˜ ์ค‘์š”๋„์™€ ๋‚˜์ด์— ๋”ฐ๋ผ)
age_factor = AGE_PROFILE.get(self.personality_stage, 1.0)
change_strength = (mem.importance / 10.0) * age_factor
# 3. ์ด๋ฒˆ ๊ธฐ์–ต์œผ๋กœ ์ธํ•œ ์ตœ์ข… ๋ณ€ํ™”๋Ÿ‰ ๋ˆ„์ 
for trait in total_influence.keys():
total_influence[trait] += influence_vector[trait] * change_strength
# 4. ๋ˆ„์ ๋œ ๋ณ€ํ™”๋Ÿ‰์„ ์„ฑ๊ฒฉ์— ์ ์šฉ
for trait, total_change in total_influence.items():
if abs(total_change) > 0.001: # ์˜๋ฏธ์žˆ๋Š” ๋ณ€ํ™”๋งŒ ์ ์šฉ
rate = self.personality_change_rate.get(trait, 0.003)
delta = total_change * rate
self.personality[trait] = max(0.0, min(1.0, self.personality[trait] + delta))
# 5. ๊ธฐ์ค€์„ ์œผ๋กœ ํšŒ๊ท€ (ํ•ญ์ƒ์„ฑ)
base_decay = 0.001
for trait in self.personality.keys():
decay_rate = base_decay * (1.0 + self.personality.get("stoic", 0.0) - self.personality.get("sensitive", 0.0))
decay_rate = max(0.0005, min(decay_rate, 0.005))
self.personality[trait] += (self.personality_baseline[trait] - self.personality[trait]) * decay_rate
print(f"[Personality Update] {self.korean_name} โ†’ sensitive: {self.personality['sensitive']:.3f}, "
f"stoic: {self.personality['stoic']:.3f}, cognitive_bias: {self.personality['cognitive_bias']:.3f}", flush=True)
def decay_memories(self):
"""
์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ๊ธฐ์–ต importance ๊ฐ์†Œ
- MemoryStore์˜ decay_memories์— self(NPC ๊ฐ์ฒด)๋ฅผ ์ „๋‹ฌ
"""
self.memory_store.decay_memories(npc=self)
def update_personality_stage(self):
"""
ํ˜„์žฌ ๋‚˜์ด์— ๋”ฐ๋ผ personality_stage ์ž๋™ ์—…๋ฐ์ดํŠธ
"""
if self.age < 3:
self.personality_stage = "infancy_and_toddlerhood"
elif self.age < 6:
self.personality_stage = "early_childhood"
elif self.age < 12:
self.personality_stage = "middle_childhood"
elif self.age < 18:
self.personality_stage = "adolescence"
elif self.age < 30:
self.personality_stage = "young_adulthood"
elif self.age < 65:
self.personality_stage = "middle_adulthood"
else:
self.personality_stage = "older_adulthood"
def get_memory_importance_scaling(self, memory_age_in_days: float) -> float:
"""
NPC์˜ ๋‚˜์ด์— ๋”ฐ๋ผ memory_age_in_days ๊ธฐ๋ฐ˜ scaling factor ๋ฐ˜ํ™˜
- ์ Š์€ NPC โ†’ ์ตœ๊ทผ ๊ธฐ์–ต ๋” ๊ฐ•์กฐ
- ๋…ธ๋…„ NPC โ†’ ์˜ค๋ž˜๋œ ๊ธฐ์–ต ๋” ๊ฐ•์กฐ
"""
if self.personality_stage in [
"infancy_and_toddlerhood", "early_childhood", "middle_childhood",
"adolescence", "young_adulthood"
]:
# ์ตœ๊ทผ ๊ธฐ์–ต ๊ฐ•์กฐ โ†’ ์˜ค๋ž˜๋œ ๊ธฐ์–ต ์˜ํ–ฅ ์•ฝํ™”
return max(0.5, 1.5 - 0.05 * memory_age_in_days) # ์ตœ๋Œ€ 1.5, ์ ์  ๊ฐ์†Œ
elif self.personality_stage in [
"middle_adulthood", "older_adulthood"
]:
return min(1.5, 0.5 + 0.05 * memory_age_in_days) # ์ตœ์†Œ 0.5, ์ ์  ์ฆ๊ฐ€
else:
# fallback โ†’ ์ •์ƒํ™”
return 1.0
def summarize_and_store_memories(self, memories: List[Memory]):
"""
๊ธฐ์–ต ๋ฆฌ์ŠคํŠธ๋ฅผ ์š”์•ฝํ•˜๊ณ , NPC ์žฅ๊ธฐ ๊ธฐ์–ต์œผ๋กœ ์ €์žฅ
"""
if not memories: return
summary = summarize_memories(memories)
if "[LLM Error]" in summary: return
self.remember(
content=f"[์š”์•ฝ๋œ ๊ธฐ์–ต] {summary}",
importance=9,
emotion=self.emotion.get_dominant_emotion(),
memory_type="Summary"
)
def update_autonomous_behavior(self, time_context: str):
"""
NPC๊ฐ€ ์Šค์Šค๋กœ ํŒ๋‹จํ•˜์—ฌ ํ–‰๋™์„ ๊ฒฐ์ •ํ•˜๋Š” ์ž์œจ ํ–‰๋™ ๋กœ์ง
์ฃผ๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœ๋˜์–ด ๋ชฉํ‘œ ์„ค์ •, ๊ณ„ํš ์‹คํ–‰ ๋“ฑ์„ ๋‹ด๋‹น.
"""
# ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ผ ๊ฒฝ์šฐ, ์ž์œจ ํ–‰๋™์„ ์‹คํ–‰ํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋Š” ์•ˆ์ „์žฅ์น˜
if self.name == "player" and not self.manager.player_is_active:
return
# 1. ๊ณ„ํš ์‹คํ–‰ ๋กœ์ง์„ NPC๊ฐ€ ์ง์ ‘ ์ฒ˜๋ฆฌ
if self.planner.has_active_plan():
step = self.planner.get_current_step()
if not step:
self.planner.clear_plan()
return
print(f"[{self.korean_name}์˜ ํ–‰๋™ ๊ณ„ํš ์‹คํ–‰] {self.planner.current_plan.current_step_index + 1}/{len(self.planner.current_plan.steps)}: {step}")
# LLM์œผ๋กœ ๊ณ„ํš์„ ๋ถ„์„ํ•˜์—ฌ ํ–‰๋™์„ ๊ฒฐ์ •
plan_details = classify_and_extract_plan_details(step)
action_type = plan_details.get("action_type")
target_korean_name = plan_details.get("target")
# ํ–‰๋™ ์‹คํ–‰ ์ „ '์กฐ๊ฑด ํ™•์ธ'๋‹จ๊ณ„
can_execute = True
if target_korean_name == "ํ”Œ๋ ˆ์ด์–ด" and not self.manager.player_is_active:
# ๋ชฉํ‘œ ๋Œ€์ƒ์ด 'ํ”Œ๋ ˆ์ด์–ด'์ธ๋ฐ, ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ผ ๊ฒฝ์šฐ
can_execute = False
simulation_core.add_log(f"[{self.korean_name}์˜ ์ƒ๊ฐ] ์ง€๊ธˆ์€ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์—†์œผ๋‹ˆ, '{step}' ๊ณ„ํš์€ ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์•ผ๊ฒ ๋‹ค.")
return
# ํ–‰๋™ ์‹คํ–‰
if action_type == "TALK" and target_korean_name:
target_npc = self.manager.get_npc_by_korean_name(target_korean_name)
if target_npc and target_npc != self:
# ์ฃผ์ œ๊ฐ€ ์žˆ๋Š” ๋Œ€ํ™” ์‹œ์ž‘
simulation_core.conversation_manager.start_conversation(self, target_npc, topic=plan_details.get("topic"))
elif action_type == "HELP" and target_korean_name:
target_npc = self.manager.get_npc_by_korean_name(target_korean_name)
target_postposition = get_korean_postposition(target_korean_name, "์ด", "๊ฐ€")
target_postposition_Eul = get_korean_postposition(target_korean_name, "์„", "๋ฅผ")
if target_npc and target_npc != self:
# '๋•๊ธฐ' ํ–‰๋™์˜ ๊ฒฐ๊ณผ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
outcome = evaluate_social_action_outcome(self, target_npc, "HELP")
summary = outcome.get('outcome_summary')
# ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์–‘์ชฝ ๋ชจ๋‘์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ
with simulation_core.simulation_lock:
# ํ–‰๋™์ž(๋‚˜)์˜ ๊ธฐ์–ต๊ณผ ๊ด€๊ณ„ ์—…๋ฐ์ดํŠธ
self.remember(content=f"{target_korean_name}{target_postposition_Eul} ๋„์™”๋‹ค. ({summary})", importance=8, emotion=outcome.get('initiator_emotion'))
self.relationships.update_relationship(target_npc.name, outcome.get('initiator_emotion'), strength=3.0)
# ๋Œ€์ƒ์ž(์ƒ๋Œ€๋ฐฉ)์˜ ๊ธฐ์–ต๊ณผ ๊ด€๊ณ„ ์—…๋ฐ์ดํŠธ
target_npc.remember(content=f"{self.korean_name}{target_postposition} ๋‚˜๋ฅผ ๋„์™€์ฃผ์—ˆ๋‹ค. ({summary})")
target_npc.relationships.update_relationship(self.name, outcome.get('target_emotion'), strength=3.0)
else: # SOLOT_ACTION ๋˜๋Š” ๊ธฐํƒ€
# ๋Œ€ํ™”๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ํ–‰๋™์€ ๊ทธ๋Œ€๋กœ ๊ธฐ์–ต์—๋งŒ ๊ธฐ๋ก
self.remember(
content=f"'{self.planner.current_goal.description}' ๋ชฉํ‘œ๋ฅผ ์œ„ํ•ด ํ˜ผ์ž ํ–‰๋™ ํ–ˆ๋‹ค: {step}",
importance = 6,
emotion="engagement",
memory_type="Behavior"
)
# ํ˜„์žฌ ๋‹จ๊ณ„ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
self.planner.complete_step()
# 2. ๊ณ„ํš์ด ์—†์œผ๋ฉด ์ƒˆ๋กœ์šด ๋ชฉํ‘œ๋ฅผ ์ƒ์„ฑํ•˜๋„๋ก ์‹œ๋„
else:
# ์†Œ๋ฌธ์„ ํผ๋œจ๋ฆฌ๋ ค๋Š” ์‹œ๋„
gossip_memories = [m for m in self.memory_store.get_all_memories() if m.memory_type == "Gossip" and not m.is_shared]
if gossip_memories and random.random() < 0.2: # 20% ํ™•๋ฅ ๋กœ ์†Œ๋ฌธ ์ „ํŒŒ ์‹œ๋„
gossip_to_spread = random.choice(gossip_memories)
# ์•„์ง ๋Œ€ํ™”ํ•œ ์  ์—†๋Š” NPC๋ฅผ ํƒ€๊ฒŸ์œผ๋กœ ์„ ์ •
potential_targets = [npc for npc in self.manager.get_interactive_npcs() if npc.name != self.name]
if potential_targets:
target_npc = random.choice(potential_targets)
# LLM์„ ํ™œ์šฉํ•˜์—ฌ ์†Œ๋ฌธ์„ ํผ๋œจ๋ฆฌ๋Š” ๋Œ€ํ™” ์ƒ์„ฑ
gossip_dialogue = self.generate_dialogue(
user_input=f"'{target_npc.korean_name}'์—๊ฒŒ '{gossip_to_spread.content}'์— ๋Œ€ํ•ด ๋„Œ์ง€์‹œ ์ด์•ผ๊ธฐํ•œ๋‹ค.",
time_context=time_context,
target_npc=target_npc,
create_memory=False
)
if gossip_dialogue and "[LLM Error]" not in gossip_dialogue:
simulation_core.add_log(f"[์†Œ๋ฌธ ์ „ํŒŒ] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
gossip_to_spread.is_shared = True
# 2. '์†Œ๋ฌธ ์ „ํŒŒ'๋ผ๋Š” ํ–‰๋™์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ๊ธฐ์–ต์„ ๋ณ„๋„๋กœ ์ƒ์„ฑ
self.remember(
content=f"{target_npc.korean_name}์—๊ฒŒ '{gossip_to_spread.content}'๋ผ๋Š” ์†Œ๋ฌธ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ–ˆ๋‹ค.",
importance=5,
emotion="engagement", # '๋‹ดํ•ฉ'๊ณผ ๊ฐ™์€ ์‚ฌํšŒ์  ํ–‰๋™์— ๋Œ€ํ•œ ๊ฐ์ •
memory_type="Gossip_Spreading" # '์†Œ๋ฌธ ์ „ํŒŒ'๋ผ๋Š” ๋ณ„๋„์˜ ํƒ€์ž… ์ง€์ •
)
# ์ผ์ • ํ™•๋ฅ ๋กœ ์ƒˆ๋กœ์šด ๋ชฉํ‘œ๋ฅผ ์ƒ์„ฑ (๋งค๋ฒˆ ์ƒ์„ฑํ•˜์ง€ ์•Š๋„๋ก)
if random.random() < 0.05: # 5% ํ™•๋ฅ 
self.planner.generate_goal()
elif random.random() < 0.1: # 10% ํ™•๋ฅ ๋กœ ๋‹ค๋ฅธ NPC์—๊ฒŒ ๊ฐ€๋ฒผ์šด ์ธ์‚ฌ ์‹œ๋„
potential_targets = [npc for npc in self.manager.get_interactive_npcs() if npc != self]
if potential_targets:
weights = []
# ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ ๋กœ์ง
for target in potential_targets:
# ๊ด€๊ณ„ ์ ์ˆ˜๋ฅผ -100 ~ 100์—์„œ 0~2 ๋ฒ”์œ„๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๊ธฐ๋ณธ ๊ฐ€์ค‘์น˜ 1์— ๋”ํ•จ
# score 100 -> weight 6 / score 0 -> weight 1 / score -100 -> weight 0.1 (์ตœ์†Œ ํ™•๋ฅ )
score = self.relationships.get_relationship_score(target.name)
weight = 1 + (score / 20.0)
weights.append(max(0.1, weight)) # ๊ฐ€์ค‘์น˜๊ฐ€ 0์ด ๋˜์ง€ ์•Š๋„๋ก ์ตœ์†Œ๊ฐ’ ๋ณด์žฅ
# ๊ฐ€์ค‘์น˜์— ๋”ฐ๋ผ ๋Œ€ํ™” ์ƒ๋Œ€ 1๋ช… ์„ ํƒ
target_npc = random.choices(potential_targets, weights=weights, k=1)[0]
# ์ฃผ์ œ๊ฐ€ ์—†๋Š” ๊ฐ€๋ฒผ์šด ๋Œ€ํ™” ์‹œ์ž‘
simulation_core.conversation_manager.start_conversation(self, target_npc)
def create_symbolic_memory(self):
"""
์ตœ๊ทผ ๊ฒฝํ—˜๊ณผ ์„ฑ๊ฒฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋™์ ์œผ๋กœ ์„ฑ์ฐฐ ์ฃผ์ œ๋ฅผ ์ •ํ•˜๊ณ , ๊ฐ€์น˜๊ด€์„ ํ˜•์„ฑ
"""
simulation_core.add_log(f"[{self.korean_name}: ๊นŠ์€ ์ƒ๊ฐ์— ์ž ๊น๋‹ˆ๋‹ค...]")
# 1. ์ตœ๊ทผ ๊ธฐ์–ต 10๊ฐœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
recent_memories = self.memory_store.get_all_memories()[-10:]
if len(recent_memories) < 8: # ์ถฉ๋ถ„ํ•œ ๊ธฐ์–ต์ด ์Œ“์—ฟ์„ ๋•Œ๋งŒ ์ƒ์„ฑ
return
# 2. ์„ฑ๊ฒฉ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•˜์—ฌ ์„ฑ์ฐฐ ์ฃผ์ œ ์ƒ์„ฑ
reflection_topic = generate_reflection_topic(
recent_memories,
self.personality.get_narrative_summary()
)
simulation_core.add_log(f"[{self.korean_name}์˜ ์„ฑ์ฐฐ ์ฃผ์ œ] {reflection_topic}")
# 3. ์ƒ์„ฑ๋œ ์ฃผ์ œ์™€ ๊ฐ€์žฅ ๊ด€๋ จ ์žˆ๋Š” ๊ธฐ์–ต๋“ค์„ ๋‹ค์‹œ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
_, _, relevant_memories = search_similar_memories(self, reflection_topic, top_k=10)
if not relevant_memories:
relevant_memories = recent_memories # ๊ด€๋ จ ๊ธฐ์–ต์ด ์—†์œผ๋ฉด ์ตœ๊ทผ ๊ธฐ์–ต์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
# 4. ๊ด€๋ จ ๊ธฐ์–ต๋“ค์„ ์š”์•ฝ
memory_summary = summarize_memories(relevant_memories)
# 5. ์š”์•ฝ๋œ ๊ธฐ์–ต๊ณผ ์„ฑ์ฐฐ ์ฃผ์ œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ข… ๊นจ๋‹ฌ์Œ์„ ์–ป์Šต๋‹ˆ๋‹ค.
prompt = f"""
# Instructions
- ๋‹น์‹ ์€ '{self.korean_name}'์ž…๋‹ˆ๋‹ค.
- ์•„๋ž˜์˜ ์ •๋ณด๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ, ๋‹น์‹ ์˜ ์ธ์ƒ์— ๋Œ€ํ•œ ๊นŠ์€ ๊นจ๋‹ฌ์Œ์ด๋‚˜ ์‹ ๋…์„ ํ•œ ๋ฌธ์žฅ์˜ '์ƒ์ง•์ ์ธ ๊ธฐ์–ต'์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”.
# Reflection Topic
"{reflection_topic}"
# Summary of Related Memories
{memory_summary}
# My Realization (a one-sentence lesson or core belief):
"""
symbolic_memory_content = query_llm_with_prompt(prompt).strip()
if symbolic_memory_content and "[LLM Error]" not in symbolic_memory_content:
self.remember(
content=symbolic_memory_content,
importance=10,
memory_type="Symbolic",
emotion="realization" # ๊นจ๋‹ฌ์Œ์ด๋ผ๋Š” ์ƒˆ๋กœ์šด ๊ฐ์ •
)
simulation_core.add_log(f"[{self.korean_name}์˜ ์ƒˆ๋กœ์šด ๊นจ๋‹ฌ์Œ] {symbolic_memory_content}")
def update_reputation(self, target_name: str, score_change: int):
"""ํŠน์ • ๋Œ€์ƒ์— ๋Œ€ํ•œ ํ‰ํŒ ์ ์ˆ˜๋ฅผ ๋ณ€๊ฒฝ"""
if self.name == target_name: return # ์ž๊ธฐ ์ž์‹ ์— ๋Œ€ํ•œ ํ‰ํŒx (์ž๊ธฐ ์ž์‹ ์˜ ํ‰ํŒ์— ๋Œ€ํ•œ ๋‚ด์šฉ๋„ ์„ฑ๊ฒฉ ํ˜•์„ฑ์— ์ค‘์š”ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š๋‚˜?)
current_score = self.reputation.get(target_name, 0)
self.reputation[target_name] = current_score + score_change
target_npc = self.manager.get_npc_by_name(target_name)
target_korean_name = target_npc.korean_name if target_npc else target_name
print(f"[{self.korean_name}]์˜ {target_korean_name}์— ๋Œ€ํ•œ ํ‰ํŒ: {self.reputation[target_name]} ({score_change:+.0f})")
def process_gossip_and_update_reputation(self, gossip_memory: Memory):
"""LLM์„ ์ด์šฉํ•ด ์†Œ๋ฌธ์„ ๋ถ„์„ํ•˜๊ณ  ๊ด€๋ จ ์ธ๋ฌผ๋“ค์˜ ํ‰ํŒ์„ ์—…๋ฐ์ดํŠธ"""
analysis = analyze_gossip(gossip_memory.content)
if not analysis:
return
person1_korean_name = analysis.get("person1")
person2_korean_name = analysis.get("person2")
sentiment = analysis.get("sentiment")
if not all([person1_korean_name, person2_korean_name, sentiment]):
return
score_change = 0
if sentiment == "positive":
score_change = 2
elif sentiment == "negative":
score_change = -2
if score_change != 0:
# LLM์ด ๋ฐ˜ํ™˜ํ•œ ํ•œ๊ธ€ ์ด๋ฆ„์œผ๋กœ NPC ๊ฐ์ฒด๋ฅผ ์ฐพ์•„ ์˜์–ด ID๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
p1_npc = self.manager.get_npc_by_korean_name(person1_korean_name)
p2_npc = self.manager.get_npc_by_korean_name(person2_korean_name)
# ์†Œ๋ฌธ์˜ ๋Œ€์ƒ์ด ๋œ ๋‘ ์‚ฌ๋žŒ์— ๋Œ€ํ•œ ๋‚˜์˜ ํ‰ํŒ์„ ๋ณ€๊ฒฝ
if p1_npc:
self.update_reputation(p1_npc.name, score_change)
if p2_npc:
self.update_reputation(p2_npc.name, score_change)