Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
d57920f
1
Parent(s):
20300ba
NPC 수정 진행 및 1차 정상 작동 확인
Browse files- npc_social_network/data/saves/simulation_state.pkl +0 -0
- npc_social_network/data/vectorstores/alice.faiss +0 -0
- npc_social_network/data/vectorstores/bob.faiss +0 -0
- npc_social_network/data/vectorstores/charlie.faiss +0 -0
- npc_social_network/data/vectorstores/diana.faiss +0 -0
- npc_social_network/data/vectorstores/elin.faiss +0 -0
- npc_social_network/models/llm_helper.py +32 -1
- npc_social_network/models/llm_prompt_builder.py +17 -6
- npc_social_network/npc/npc_base.py +64 -31
- npc_social_network/npc/npc_manager.py +24 -8
- npc_social_network/npc/npc_planner.py +10 -12
- npc_social_network/npc/npc_relationship.py +9 -5
- npc_social_network/routes/npc_route.py +8 -17
- npc_social_network/scenarios/scenario_setup.py +16 -11
- npc_social_network/simulation_core.py +34 -2
npc_social_network/data/saves/simulation_state.pkl
CHANGED
Binary files a/npc_social_network/data/saves/simulation_state.pkl and b/npc_social_network/data/saves/simulation_state.pkl differ
|
|
npc_social_network/data/vectorstores/alice.faiss
ADDED
Binary file (1.58 kB). View file
|
|
npc_social_network/data/vectorstores/bob.faiss
ADDED
Binary file (1.58 kB). View file
|
|
npc_social_network/data/vectorstores/charlie.faiss
ADDED
Binary file (1.58 kB). View file
|
|
npc_social_network/data/vectorstores/diana.faiss
ADDED
Binary file (1.58 kB). View file
|
|
npc_social_network/data/vectorstores/elin.faiss
ADDED
Binary file (1.58 kB). View file
|
|
npc_social_network/models/llm_helper.py
CHANGED
@@ -4,6 +4,8 @@ from npc_social_network.npc.emotion_config import EMOTION_LIST
|
|
4 |
from npc_social_network.models.gemini_setup import load_gemini
|
5 |
from npc_social_network.npc.npc_memory import Memory
|
6 |
from typing import List
|
|
|
|
|
7 |
|
8 |
# Gemini 모델 초기화
|
9 |
gemini_model = load_gemini()
|
@@ -55,4 +57,33 @@ def summarize_memories(memories: List[Memory]) -> str:
|
|
55 |
|
56 |
# LLM 호출 함수를 사용하여 요약 요청
|
57 |
summary = query_llm_with_prompt(prompt)
|
58 |
-
return summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
from npc_social_network.models.gemini_setup import load_gemini
|
5 |
from npc_social_network.npc.npc_memory import Memory
|
6 |
from typing import List
|
7 |
+
import json
|
8 |
+
import re
|
9 |
|
10 |
# Gemini 모델 초기화
|
11 |
gemini_model = load_gemini()
|
|
|
57 |
|
58 |
# LLM 호출 함수를 사용하여 요약 요청
|
59 |
summary = query_llm_with_prompt(prompt)
|
60 |
+
return summary
|
61 |
+
|
62 |
+
def analyze_gossip(gossip_content: str) -> dict:
|
63 |
+
"""LLM을 이용해 소문을 분석하고, 관련 인물과 긍/부정을 JSON으로 반환합니다."""
|
64 |
+
prompt="""
|
65 |
+
# 지시사항
|
66 |
+
다음 소문 내용에서 핵심 인물 2명과, 그들 사이의 상호작용이 긍정적인지, 부정적인지, 중립적인지 분석해줘.
|
67 |
+
결과를 반드시 아래 JSON 형식으로만 응답해줘, 다른 설명은 붙이지 마.
|
68 |
+
|
69 |
+
# 소문 내용
|
70 |
+
"{gossip_content}"
|
71 |
+
|
72 |
+
# 분석 결과 (JSON 형식)
|
73 |
+
{{
|
74 |
+
"person1": "첫 번째 인물 이름",
|
75 |
+
"person2": "두 번째 인물 이름",
|
76 |
+
"sentiment": "positive or negative or neutral",
|
77 |
+
"reason": "한 문장으로 요약한 이유"
|
78 |
+
}}
|
79 |
+
"""
|
80 |
+
try:
|
81 |
+
response_text = query_llm_with_prompt(prompt)
|
82 |
+
# LLM 응답에서 JSON 부분만 추출
|
83 |
+
json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
|
84 |
+
if json_match:
|
85 |
+
return json.loads(json_match.group())
|
86 |
+
return {}
|
87 |
+
except (json.JSONDecodeError, Exception) as e:
|
88 |
+
print(f"소문 분석 중 오류 발생: {e}")
|
89 |
+
return {}
|
npc_social_network/models/llm_prompt_builder.py
CHANGED
@@ -15,7 +15,8 @@ def build_npc_prompt(npc:"NPC", user_input: str, matched_memories: List[Memory],
|
|
15 |
- SocialProfile의 관꼐 요약을 프롬프트에 포함하도록 수정
|
16 |
"""
|
17 |
# 대화 상대방의 이름을 동적으로 설정
|
18 |
-
interlocutor_name = target_npc.name if target_npc else "
|
|
|
19 |
|
20 |
# 감정 상태 요약
|
21 |
emotion_summary = npc.summarize_emotional_state()
|
@@ -24,6 +25,15 @@ def build_npc_prompt(npc:"NPC", user_input: str, matched_memories: List[Memory],
|
|
24 |
# 관계 요약
|
25 |
relationship_description = npc.get_relationship_description(interlocutor_name)
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
# 기억 유무에 따른 동적 지침 생성
|
28 |
if matched_memories:
|
29 |
memory_section_title = "# 연관된 기억"
|
@@ -65,15 +75,16 @@ def build_npc_prompt(npc:"NPC", user_input: str, matched_memories: List[Memory],
|
|
65 |
{instruction}
|
66 |
|
67 |
# 나의 프로필
|
68 |
-
- 이름: {npc.
|
69 |
- 직업: {npc.job}
|
70 |
-
- 현재 대화 상대: {
|
71 |
- 현재 시간대: {time_context}
|
72 |
|
73 |
# 나의 내면 상태
|
74 |
- 현재 감정: {emotion_summary}
|
75 |
- 성격: {personality_summary}
|
76 |
-
- {
|
|
|
77 |
|
78 |
{symbolic_section}
|
79 |
|
@@ -81,7 +92,7 @@ def build_npc_prompt(npc:"NPC", user_input: str, matched_memories: List[Memory],
|
|
81 |
{memory_section}
|
82 |
|
83 |
# 대화 로그
|
84 |
-
{
|
85 |
-
{npc.
|
86 |
"""
|
87 |
return prompt
|
|
|
15 |
- SocialProfile의 관꼐 요약을 프롬프트에 포함하도록 수정
|
16 |
"""
|
17 |
# 대화 상대방의 이름을 동적으로 설정
|
18 |
+
interlocutor_name = target_npc.name if target_npc else "player"
|
19 |
+
interlocutor_korean_name = target_npc.korean_name if target_npc else "플레이어"
|
20 |
|
21 |
# 감정 상태 요약
|
22 |
emotion_summary = npc.summarize_emotional_state()
|
|
|
25 |
# 관계 요약
|
26 |
relationship_description = npc.get_relationship_description(interlocutor_name)
|
27 |
|
28 |
+
# 평판 정보
|
29 |
+
reputation_info = ""
|
30 |
+
if target_npc:
|
31 |
+
reputation_score = npc.reputation.get(target_npc.name, 0)
|
32 |
+
if reputation_score > 5:
|
33 |
+
reputation_info = f"- '{target_npc.korean_name}'에 대한 평판: 신뢰할 만한 사람 같다. (평판 점수: {reputation_score})"
|
34 |
+
elif reputation_score < -5:
|
35 |
+
reputation_info = f"- '{target_npc.korean_name}'에 대한 평판: 그다지 좋은 소문을 듣지 못했다. (평판 점수: {reputation_score})"
|
36 |
+
|
37 |
# 기억 유무에 따른 동적 지침 생성
|
38 |
if matched_memories:
|
39 |
memory_section_title = "# 연관된 기억"
|
|
|
75 |
{instruction}
|
76 |
|
77 |
# 나의 프로필
|
78 |
+
- 이름: {npc.korean_name}
|
79 |
- 직업: {npc.job}
|
80 |
+
- 현재 대화 상대: {interlocutor_korean_name}
|
81 |
- 현재 시간대: {time_context}
|
82 |
|
83 |
# 나의 내면 상태
|
84 |
- 현재 감정: {emotion_summary}
|
85 |
- 성격: {personality_summary}
|
86 |
+
- {interlocutor_korean_name}와(과)의 관계: {relationship_description}
|
87 |
+
- {reputation_info}
|
88 |
|
89 |
{symbolic_section}
|
90 |
|
|
|
92 |
{memory_section}
|
93 |
|
94 |
# 대화 로그
|
95 |
+
{interlocutor_korean_name}: "{user_input}"
|
96 |
+
{npc.korean_name}:
|
97 |
"""
|
98 |
return prompt
|
npc_social_network/npc/npc_base.py
CHANGED
@@ -10,10 +10,13 @@ from .emotion_config import EMOTION_LIST, EMOTION_CATEGORY_MAP, EMOTION_DECAY_RA
|
|
10 |
from .emotion_config import POSITIVE_RELATION_EMOTIONS, NEGATIVE_RELATION_EMOTIONS, COGNITIVE_RELATION_EMOTIONS
|
11 |
from .personality_config import AGE_PROFILE, PERSONALITY_PROFILE
|
12 |
from .npc_relationship import RelationshipManager
|
13 |
-
from ..models.llm_helper import query_llm_with_prompt, query_llm_for_emotion, summarize_memories
|
14 |
from ..models.llm_prompt_builder import build_npc_prompt
|
15 |
from .npc_memory_embedder import search_similar_memories
|
16 |
from .npc_planner import PlannerManager
|
|
|
|
|
|
|
17 |
|
18 |
|
19 |
# Personality 데이터를 관리하고 요약 기능을 제공하는 클래스 정의
|
@@ -73,8 +76,10 @@ class NPC:
|
|
73 |
NPC 클래스
|
74 |
NPC에 대한 기본 정보를 객체화
|
75 |
"""
|
76 |
-
def __init__(self, name: str,
|
|
|
77 |
self.name = name
|
|
|
78 |
self.job = job
|
79 |
|
80 |
# npc 기억
|
@@ -83,7 +88,7 @@ class NPC:
|
|
83 |
# 감정 상태 등 감정 관리자 초기화
|
84 |
self.personality = PersonalityManager(personality)
|
85 |
self.relationships = RelationshipManager(self)
|
86 |
-
self.
|
87 |
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
|
88 |
# 행동 관리자
|
89 |
self.behavior = BehaviorManager()
|
@@ -157,8 +162,9 @@ class NPC:
|
|
157 |
self.update_emotion(emotion, strength=2.0)
|
158 |
|
159 |
# 5. 상호작용 기억 저장
|
160 |
-
interlocutor_name = target_npc.name if target_npc else "
|
161 |
-
|
|
|
162 |
memory = self.remember(content=memory_content, importance=7, emotion=emotion, context_tags=[time_context])
|
163 |
|
164 |
# 6. 관계 상태 업데이트
|
@@ -209,7 +215,7 @@ class NPC:
|
|
209 |
|
210 |
def remember(self, content: str, importance: int = 5, emotion: str = None,
|
211 |
strength: float = 1.0, memory_type:str = "Event",
|
212 |
-
context_tags: Optional[List[str]]=None) -> Memory:
|
213 |
"""
|
214 |
NPC가 새로운 기억을 저장하고 감정 상태에 반영
|
215 |
"""
|
@@ -224,22 +230,12 @@ class NPC:
|
|
224 |
if emotion and emotion in EMOTION_LIST:
|
225 |
self.update_emotion(emotion, strength) # strength = importance / 5.0으로도 가능
|
226 |
# 만약 기억이 '소문'이라면, 평판을 업데이트
|
227 |
-
if memory_type == "Gossip":
|
228 |
# (간단한 예시: LLM으로 소문의 대상과 긍/부정을 파악해야 더 정확함)
|
229 |
# 긍정 소문 예시: "밥이 찰리를 도왔다"
|
230 |
# 부정 소문 예시: "엘린이 앨리스와 다퉜다"
|
231 |
-
|
232 |
-
|
233 |
-
if "도왔다" in content or "칭찬했다" in content:
|
234 |
-
# 긍정적 소문의 대상을 찾아 평판 +1
|
235 |
-
# ... (대상 파싱 로직) ...
|
236 |
-
pass
|
237 |
-
elif "다퉜다" in content or "비난했다" in content:
|
238 |
-
# 부정적 소문의 대상들을 찾아 평판 -1
|
239 |
-
# ... (대상 파싱 로직) ...
|
240 |
-
# 예시: self.update_reputation("엘린", -1)
|
241 |
-
# self.update_reputation("앨리스", -1)
|
242 |
-
pass
|
243 |
return memory # 생성된 기억 객체 반환
|
244 |
|
245 |
def update_emotion(self, emotion:str, strength: float = 1.0):
|
@@ -406,13 +402,13 @@ class NPC:
|
|
406 |
summary = summarize_memories(memories)
|
407 |
if "[LLM Error]" in summary: return
|
408 |
self.remember(
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
)
|
414 |
|
415 |
-
def update_autonomous_behavior(self, time_context: str):
|
416 |
"""
|
417 |
NPC가 스스로 판단하여 행동을 결정하는 자율 행동 로직
|
418 |
주기적으로 호출되어 목표 설정, 계획 실행 등을 담당.
|
@@ -438,18 +434,18 @@ class NPC:
|
|
438 |
gossip_to_spread = random.choice(gossip_memories)
|
439 |
|
440 |
# 아직 대화한 적 없는 NPC를 타겟으로 선정
|
441 |
-
potential_targets = [npc for npc in npc_manager.all() if npc.name != self.name]
|
442 |
if potential_targets:
|
443 |
target_npc = random.choice(potential_targets)
|
444 |
|
445 |
# LLM을 활용하여 소문을 퍼뜨리는 대화 생성
|
446 |
gossip_dialogue = self.generate_dialogue(
|
447 |
-
user_input=f"'{target_npc.
|
448 |
time_context=time_context,
|
449 |
target_npc=target_npc
|
450 |
)
|
451 |
|
452 |
-
add_log(f"[소문 전파] {self.
|
453 |
gossip_to_spread.is_shared = True # 소문이 전파되었음을 표시 (중복 전파 방지)
|
454 |
|
455 |
# 일정 확률로 새로운 목표를 생성 (매번 생성하지 않도록)
|
@@ -475,7 +471,7 @@ class NPC:
|
|
475 |
|
476 |
prompt = f"""
|
477 |
# 지시사항
|
478 |
-
나는 '{self.
|
479 |
이 기억들을 바탕으로, "{theme}"이라는 주제에 대해 당신의 삶을 대표하는 상징적인 의미나 깨달음을 한 문장의 독백으로 생성해주세요.
|
480 |
|
481 |
# 나의 기억들
|
@@ -494,12 +490,49 @@ class NPC:
|
|
494 |
emotion = "rumination", # 반추, 성찰과 관련된 감정
|
495 |
memory_type="Symbolic",
|
496 |
)
|
497 |
-
print(f"[{self.
|
498 |
|
499 |
-
def update_reputation(self, target_name: str, score_change: int):
|
500 |
"""특정 대상에 대한 평판 점수를 변경"""
|
501 |
if self.name == target_name: return # 자기 자신에 대한 평판x (자기 자신의 평판에 대한 내용도 성격 형성에 중요할 수 있지 않나?)
|
502 |
|
503 |
current_score = self.reputation.get(target_name, 0)
|
504 |
self.reputation[target_name] = current_score + score_change
|
505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
from .emotion_config import POSITIVE_RELATION_EMOTIONS, NEGATIVE_RELATION_EMOTIONS, COGNITIVE_RELATION_EMOTIONS
|
11 |
from .personality_config import AGE_PROFILE, PERSONALITY_PROFILE
|
12 |
from .npc_relationship import RelationshipManager
|
13 |
+
from ..models.llm_helper import query_llm_with_prompt, query_llm_for_emotion, summarize_memories, analyze_gossip
|
14 |
from ..models.llm_prompt_builder import build_npc_prompt
|
15 |
from .npc_memory_embedder import search_similar_memories
|
16 |
from .npc_planner import PlannerManager
|
17 |
+
from typing import TYPE_CHECKING
|
18 |
+
if TYPE_CHECKING:
|
19 |
+
from .npc_manager import NPCManager
|
20 |
|
21 |
|
22 |
# Personality 데이터를 관리하고 요약 기능을 제공하는 클래스 정의
|
|
|
76 |
NPC 클래스
|
77 |
NPC에 대한 기본 정보를 객체화
|
78 |
"""
|
79 |
+
def __init__(self, name: str, korean_name: str, job: str,
|
80 |
+
personality: Optional[dict]=None):
|
81 |
self.name = name
|
82 |
+
self.korean_name = korean_name
|
83 |
self.job = job
|
84 |
|
85 |
# npc 기억
|
|
|
88 |
# 감정 상태 등 감정 관리자 초기화
|
89 |
self.personality = PersonalityManager(personality)
|
90 |
self.relationships = RelationshipManager(self)
|
91 |
+
self.reputation = {} # 다른 NPC에 대한 평판 저장
|
92 |
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
|
93 |
# 행동 관리자
|
94 |
self.behavior = BehaviorManager()
|
|
|
162 |
self.update_emotion(emotion, strength=2.0)
|
163 |
|
164 |
# 5. 상호작용 기억 저장
|
165 |
+
interlocutor_name = target_npc.name if target_npc else "player"
|
166 |
+
interlocutor_korean_name = target_npc.korean_name if target_npc else "플레이어"
|
167 |
+
memory_content = f"[{interlocutor_korean_name}의 말] '{user_input}' → [나의 응답] '{npc_reply}'"
|
168 |
memory = self.remember(content=memory_content, importance=7, emotion=emotion, context_tags=[time_context])
|
169 |
|
170 |
# 6. 관계 상태 업데이트
|
|
|
215 |
|
216 |
def remember(self, content: str, importance: int = 5, emotion: str = None,
|
217 |
strength: float = 1.0, memory_type:str = "Event",
|
218 |
+
context_tags: Optional[List[str]]=None, npc_manager: "NPCManager" = None) -> Memory:
|
219 |
"""
|
220 |
NPC가 새로운 기억을 저장하고 감정 상태에 반영
|
221 |
"""
|
|
|
230 |
if emotion and emotion in EMOTION_LIST:
|
231 |
self.update_emotion(emotion, strength) # strength = importance / 5.0으로도 가능
|
232 |
# 만약 기억이 '소문'이라면, 평판을 업데이트
|
233 |
+
if memory_type == "Gossip" and npc_manager:
|
234 |
# (간단한 예시: LLM으로 소문의 대상과 긍/부정을 파악해야 더 정확함)
|
235 |
# 긍정 소문 예시: "밥이 찰리를 도왔다"
|
236 |
# 부정 소문 예시: "엘린이 앨리스와 다퉜다"
|
237 |
+
self.process_gossip_and_update_reputation(memory, npc_manager)
|
238 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
return memory # 생성된 기억 객체 반환
|
240 |
|
241 |
def update_emotion(self, emotion:str, strength: float = 1.0):
|
|
|
402 |
summary = summarize_memories(memories)
|
403 |
if "[LLM Error]" in summary: return
|
404 |
self.remember(
|
405 |
+
content=f"[요약된 기억] {summary}",
|
406 |
+
importance=9,
|
407 |
+
emotion=self.emotion.get_dominant_emotion(),
|
408 |
+
memory_type="Summary"
|
409 |
)
|
410 |
|
411 |
+
def update_autonomous_behavior(self, time_context: str, npc_manager, add_log):
|
412 |
"""
|
413 |
NPC가 스스로 판단하여 행동을 결정하는 자율 행동 로직
|
414 |
주기적으로 호출되어 목표 설정, 계획 실행 등을 담당.
|
|
|
434 |
gossip_to_spread = random.choice(gossip_memories)
|
435 |
|
436 |
# 아직 대화한 적 없는 NPC를 타겟으로 선정
|
437 |
+
potential_targets = [npc for npc in npc_manager.all() if npc.name != self.name]
|
438 |
if potential_targets:
|
439 |
target_npc = random.choice(potential_targets)
|
440 |
|
441 |
# LLM을 활용하여 소문을 퍼뜨리는 대화 생성
|
442 |
gossip_dialogue = self.generate_dialogue(
|
443 |
+
user_input=f"'{target_npc.korean_name}'에게 '{gossip_to_spread.content}'에 대해 넌지시 이야기한다.",
|
444 |
time_context=time_context,
|
445 |
target_npc=target_npc
|
446 |
)
|
447 |
|
448 |
+
add_log(f"[소문 전파] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
|
449 |
gossip_to_spread.is_shared = True # 소문이 전파되었음을 표시 (중복 전파 방지)
|
450 |
|
451 |
# 일정 확률로 새로운 목표를 생성 (매번 생성하지 않도록)
|
|
|
471 |
|
472 |
prompt = f"""
|
473 |
# 지시사항
|
474 |
+
나는 '{self.korean_name}'입니다. 다음은 나의 인생 기억 중 일부입니다.
|
475 |
이 기억들을 바탕으로, "{theme}"이라는 주제에 대해 당신의 삶을 대표하는 상징적인 의미나 깨달음을 한 문장의 독백으로 생성해주세요.
|
476 |
|
477 |
# 나의 기억들
|
|
|
490 |
emotion = "rumination", # 반추, 성찰과 관련된 감정
|
491 |
memory_type="Symbolic",
|
492 |
)
|
493 |
+
print(f"[{self.korean_name}]의 새로운 깨달음: {symbolic_thought}")
|
494 |
|
495 |
+
def update_reputation(self, target_name: str, score_change: int, npc_manager: "NPCManager"):
|
496 |
"""특정 대상에 대한 평판 점수를 변경"""
|
497 |
if self.name == target_name: return # 자기 자신에 대한 평판x (자기 자신의 평판에 대한 내용도 성격 형성에 중요할 수 있지 않나?)
|
498 |
|
499 |
current_score = self.reputation.get(target_name, 0)
|
500 |
self.reputation[target_name] = current_score + score_change
|
501 |
+
|
502 |
+
target_npc = npc_manager.get_npc_by_name(target_name)
|
503 |
+
target_korean_name = target_npc.korean_name if target_npc else target_name
|
504 |
+
|
505 |
+
print(f"[{self.korean_name}]의 {target_korean_name}에 대한 평판: {self.reputation[target_name]} ({score_change:+.0f})")
|
506 |
+
|
507 |
+
|
508 |
+
def process_gossip_and_update_reputation(self, gossip_memory: Memory, npc_manager: "NPCManager"):
|
509 |
+
"""LLM을 이용해 소문을 분석하고 관련 인물들의 평판을 업데이트"""
|
510 |
+
|
511 |
+
analysis = analyze_gossip(gossip_memory.content)
|
512 |
+
if not analysis:
|
513 |
+
return
|
514 |
+
|
515 |
+
person1_korean_name = analysis.get("person1")
|
516 |
+
person2_korean_name = analysis.get("person2")
|
517 |
+
sentiment = analysis.get("sentiment")
|
518 |
+
|
519 |
+
if not all([person1_korean_name, person2_korean_name, sentiment]):
|
520 |
+
return
|
521 |
+
|
522 |
+
score_change = 0
|
523 |
+
|
524 |
+
if sentiment == "positive":
|
525 |
+
score_change = 2
|
526 |
+
elif sentiment == "negative":
|
527 |
+
score_change = -2
|
528 |
+
|
529 |
+
if score_change != 0:
|
530 |
+
# LLM이 반환한 한글 이름으로 NPC 객체를 찾아 영어 ID를 가져온다.
|
531 |
+
p1_npc = npc_manager.get_npc_by_korean_name(person1_korean_name)
|
532 |
+
p2_npc = npc_manager.get_npc_by_korean_name(person2_korean_name)
|
533 |
+
|
534 |
+
# 소문의 대상이 된 두 사람에 대한 나의 평판을 변경
|
535 |
+
if p1_npc:
|
536 |
+
self.update_reputation(p1_npc.name, score_change, npc_manager)
|
537 |
+
if p2_npc:
|
538 |
+
self.update_reputation(p2_npc.name, score_change, npc_manager)
|
npc_social_network/npc/npc_manager.py
CHANGED
@@ -5,24 +5,37 @@ from typing import TYPE_CHECKING, List, Optional
|
|
5 |
if TYPE_CHECKING:
|
6 |
from .npc_base import NPC
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
class NPCManager:
|
9 |
"""모든 NPC를 관리하는 중앙 관리자"""
|
10 |
def __init__(self):
|
11 |
self.npcs: list['NPC'] = []
|
12 |
self.npc_dict: dict[str, 'NPC'] = {}
|
|
|
13 |
|
14 |
def add_npc(self, npc: 'NPC'):
|
15 |
"""NPC를 매니저에 추가합니다."""
|
16 |
if npc.name not in self.npc_dict:
|
17 |
self.npcs.append(npc)
|
18 |
self.npc_dict[npc.name] = npc
|
|
|
19 |
|
20 |
def get_npc_by_name(self, name: str) -> Optional['NPC']:
|
21 |
"""
|
22 |
-
NPC
|
23 |
"""
|
24 |
return self.npc_dict.get(name)
|
25 |
|
|
|
|
|
|
|
|
|
26 |
def get_random_npc(self, exclude: Optional['NPC']=None) -> Optional['NPC']:
|
27 |
"""
|
28 |
특정 NPC를 제외하고 랜덤한 NPC를 선택
|
@@ -55,27 +68,30 @@ class NPCManager:
|
|
55 |
if not initiator or not target:
|
56 |
return
|
57 |
|
58 |
-
|
|
|
|
|
|
|
59 |
|
60 |
# 1. Initiator가 Target에 대해 무슨 말을 할지 LLM으로 생성
|
61 |
prompt = f"""
|
62 |
# 지시사항
|
63 |
-
당신은 '{initiator.
|
64 |
-
'{target.
|
65 |
(예: "앨리스, 어제 시장에서 파는 그 옷 정말 예쁘더라.", "밥, 요즘 허리가 아프다던데 괜찮아?")
|
66 |
|
67 |
# 당신의 정보
|
68 |
-
- '{target.
|
69 |
|
70 |
→ 첫 마디:
|
71 |
"""
|
72 |
initial_utterance = query_llm_with_prompt(prompt).replace("'", "").strip()
|
73 |
|
74 |
if "[LLM Error]" in initial_utterance:
|
75 |
-
print(f"[{initiator.
|
76 |
return None, None
|
77 |
|
78 |
-
print(f"[{initiator.
|
79 |
|
80 |
# 2. Target이 Initiator의 말을 듣고 응답 생성
|
81 |
# generate_dialogue 함수를 재사용하되, target_npc 인자를 전달
|
@@ -85,7 +101,7 @@ class NPCManager:
|
|
85 |
target_npc=initiator
|
86 |
)
|
87 |
|
88 |
-
print(f"[{target.
|
89 |
print("-------------------------\n")
|
90 |
return initiator, target, initial_utterance
|
91 |
|
|
|
5 |
if TYPE_CHECKING:
|
6 |
from .npc_base import NPC
|
7 |
|
8 |
+
def get_korean_postposition(name, first_char, second_char):
|
9 |
+
"""이름의 마지막 글자 받침 유무에 따라 올바른 조사를 반환"""
|
10 |
+
if (ord(name[-1]) - 0xAC00) % 28 > 0:
|
11 |
+
return first_char
|
12 |
+
else:
|
13 |
+
return second_char
|
14 |
+
|
15 |
class NPCManager:
|
16 |
"""모든 NPC를 관리하는 중앙 관리자"""
|
17 |
def __init__(self):
|
18 |
self.npcs: list['NPC'] = []
|
19 |
self.npc_dict: dict[str, 'NPC'] = {}
|
20 |
+
self.korean_name_to_npc: dict[str, 'NPC'] = {} # 한글 이름 검색용 딕셔너리
|
21 |
|
22 |
def add_npc(self, npc: 'NPC'):
|
23 |
"""NPC를 매니저에 추가합니다."""
|
24 |
if npc.name not in self.npc_dict:
|
25 |
self.npcs.append(npc)
|
26 |
self.npc_dict[npc.name] = npc
|
27 |
+
self.korean_name_to_npc[npc.korean_name] = npc
|
28 |
|
29 |
def get_npc_by_name(self, name: str) -> Optional['NPC']:
|
30 |
"""
|
31 |
+
NPC 영어 ID를 통해서 NPC의 정보를 반환
|
32 |
"""
|
33 |
return self.npc_dict.get(name)
|
34 |
|
35 |
+
def get_npc_by_korean_name(self, korean_name: str) -> Optional['NPC']:
|
36 |
+
"""NPC 한글 이름을 통해서 NPC의 정보를 반환"""
|
37 |
+
return self.korean_name_to_npc.get(korean_name)
|
38 |
+
|
39 |
def get_random_npc(self, exclude: Optional['NPC']=None) -> Optional['NPC']:
|
40 |
"""
|
41 |
특정 NPC를 제외하고 랜덤한 NPC를 선택
|
|
|
68 |
if not initiator or not target:
|
69 |
return
|
70 |
|
71 |
+
initiator_postposition = get_korean_postposition(initiator.korean_name, "이", "가")
|
72 |
+
target_postposition = get_korean_postposition(target.korean_name, "에게", "에게")
|
73 |
+
|
74 |
+
print(f"\n---[NPC 상호작용 이벤트]---\n{initiator.korean_name}{initiator_postposition} {target.korean_name}{target_postposition} 상호작용을 시도합니다.")
|
75 |
|
76 |
# 1. Initiator가 Target에 대해 무슨 말을 할지 LLM으로 생성
|
77 |
prompt = f"""
|
78 |
# 지시사항
|
79 |
+
당신은 '{initiator.korean_name}'입니다. 지금 당신은 '{target.korean_name}'와 마주쳤습니다.
|
80 |
+
'{target.korean_name}'에 대한 당신의 현재 생각과 감정을 바탕으로, 먼저 건넬 자연스러운 첫 마디를 생성하세요.
|
81 |
(예: "앨리스, 어제 시장에서 파는 그 옷 정말 예쁘더라.", "밥, 요즘 허리가 아프다던데 괜찮아?")
|
82 |
|
83 |
# 당신의 정보
|
84 |
+
- '{target.korean_name}'와의 관계: {initiator.relationships.get_relationship_summary(target.name)}
|
85 |
|
86 |
→ 첫 마디:
|
87 |
"""
|
88 |
initial_utterance = query_llm_with_prompt(prompt).replace("'", "").strip()
|
89 |
|
90 |
if "[LLM Error]" in initial_utterance:
|
91 |
+
print(f"[{initiator.korean_name}] 대화 시작에 실패했습니다.")
|
92 |
return None, None
|
93 |
|
94 |
+
print(f"[{initiator.korean_name}]: {initial_utterance}")
|
95 |
|
96 |
# 2. Target이 Initiator의 말을 듣고 응답 생성
|
97 |
# generate_dialogue 함수를 재사용하되, target_npc 인자를 전달
|
|
|
101 |
target_npc=initiator
|
102 |
)
|
103 |
|
104 |
+
print(f"[{target.korean_name}]: {response_utterance}")
|
105 |
print("-------------------------\n")
|
106 |
return initiator, target, initial_utterance
|
107 |
|
npc_social_network/npc/npc_planner.py
CHANGED
@@ -47,17 +47,17 @@ class PlannerManager:
|
|
47 |
|
48 |
# LLM에게 전달할 NPC 상태 요약
|
49 |
state_summary = f"""
|
50 |
-
#
|
51 |
- 주요 감정: {self.npc.summarize_emotional_state()}
|
52 |
- 최근 기억 3개:
|
53 |
{chr(10).join([f' - {mem.content}' for mem in self.npc.memory_store.get_recent_memories(limit=3)])}
|
54 |
-
- 플레이어와의 관계: {self.npc.get_relationship_description("
|
55 |
"""
|
56 |
|
57 |
prompt = f"""
|
58 |
# 지시사항
|
59 |
-
→
|
60 |
-
→ 목표는
|
61 |
|
62 |
{state_summary}
|
63 |
|
@@ -68,10 +68,10 @@ class PlannerManager:
|
|
68 |
|
69 |
if goal_description and "[LLM Error]" not in goal_description:
|
70 |
self.current_goal = Goal(description=goal_description)
|
71 |
-
print(f"[{self.npc.
|
72 |
self.create_plan_for_goal()
|
73 |
else:
|
74 |
-
print(f"[{self.npc.
|
75 |
|
76 |
def create_plan_for_goal(self):
|
77 |
"""
|
@@ -82,13 +82,11 @@ class PlannerManager:
|
|
82 |
|
83 |
prompt = f"""
|
84 |
# 지시사항
|
85 |
-
→
|
86 |
→ 이 목표를 달성하기 위한 3~5개의 구체적이고 실행 가능한 행동 단계를 번호 목록으로 작성해주세요.
|
87 |
→ 각 단계는 짧은 문장으로 표현하세요,
|
88 |
|
89 |
→ 계획:
|
90 |
-
|
91 |
-
1.
|
92 |
"""
|
93 |
plan_text = "1. " + query_llm_with_prompt(prompt).replace("계획:", "").strip()
|
94 |
steps = [step.strip() for step in plan_text.split('\n') if step.strip()]
|
@@ -98,7 +96,7 @@ class PlannerManager:
|
|
98 |
|
99 |
if cleaned_steps:
|
100 |
self.current_plan = Plan(goal=self.current_goal, steps=cleaned_steps)
|
101 |
-
print(f"[{self.npc.
|
102 |
for i, step in enumerate(self.current_plan.steps):
|
103 |
print(f" - {i+1}단계: {step}")
|
104 |
|
@@ -113,13 +111,13 @@ class PlannerManager:
|
|
113 |
step_index = plan.current_step_index
|
114 |
|
115 |
if step_index >= len(plan.steps):
|
116 |
-
print(f"[{self.npc.
|
117 |
plan.goal.status = "completed"
|
118 |
self.current_plan = None
|
119 |
return f"({plan.goal.description} 목표를 달성하여 만족스럽습니다.)"
|
120 |
|
121 |
current_step = plan.steps[step_index]
|
122 |
-
print(f"[{self.npc.
|
123 |
|
124 |
# 다음 단계로
|
125 |
plan.current_step_index += 1
|
|
|
47 |
|
48 |
# LLM에게 전달할 NPC 상태 요약
|
49 |
state_summary = f"""
|
50 |
+
# 나의 현재 상태 요약
|
51 |
- 주요 감정: {self.npc.summarize_emotional_state()}
|
52 |
- 최근 기억 3개:
|
53 |
{chr(10).join([f' - {mem.content}' for mem in self.npc.memory_store.get_recent_memories(limit=3)])}
|
54 |
+
- 플레이어와의 관계: {self.npc.get_relationship_description("player")}
|
55 |
"""
|
56 |
|
57 |
prompt = f"""
|
58 |
# 지시사항
|
59 |
+
→ 나는 '{self.npc.korean_name}'입니다. 나의 현재 상태를 바탕으로, 지금 가장 중요하게 추구추구해야 할 장기적인 목표를 한 문장으로 제안하세요.
|
60 |
+
→ 목표는 나의 삶을 더 낫게 만들거나, 문제를 해결하거나, 관계를 개선하는 것이어야 합니다.
|
61 |
|
62 |
{state_summary}
|
63 |
|
|
|
68 |
|
69 |
if goal_description and "[LLM Error]" not in goal_description:
|
70 |
self.current_goal = Goal(description=goal_description)
|
71 |
+
print(f"[{self.npc.korean_name}의 새로운 목표] {self.current_goal.description}")
|
72 |
self.create_plan_for_goal()
|
73 |
else:
|
74 |
+
print(f"[{self.npc.korean_name}] 목표 생성에 실패했습니다.")
|
75 |
|
76 |
def create_plan_for_goal(self):
|
77 |
"""
|
|
|
82 |
|
83 |
prompt = f"""
|
84 |
# 지시사항
|
85 |
+
→ 나는 '{self.npc.korean_name}'입니다. 나의 목표는 '{self.current_goal.description}'입니다.
|
86 |
→ 이 목표를 달성하기 위한 3~5개의 구체적이고 실행 가능한 행동 단계를 번호 목록으로 작성해주세요.
|
87 |
→ 각 단계는 짧은 문장으로 표현하세요,
|
88 |
|
89 |
→ 계획:
|
|
|
|
|
90 |
"""
|
91 |
plan_text = "1. " + query_llm_with_prompt(prompt).replace("계획:", "").strip()
|
92 |
steps = [step.strip() for step in plan_text.split('\n') if step.strip()]
|
|
|
96 |
|
97 |
if cleaned_steps:
|
98 |
self.current_plan = Plan(goal=self.current_goal, steps=cleaned_steps)
|
99 |
+
print(f"[{self.npc.korean_name}의 계획 수립]")
|
100 |
for i, step in enumerate(self.current_plan.steps):
|
101 |
print(f" - {i+1}단계: {step}")
|
102 |
|
|
|
111 |
step_index = plan.current_step_index
|
112 |
|
113 |
if step_index >= len(plan.steps):
|
114 |
+
print(f"[{self.npc.korean_name}] 목표 달성: {plan.goal.description}")
|
115 |
plan.goal.status = "completed"
|
116 |
self.current_plan = None
|
117 |
return f"({plan.goal.description} 목표를 달성하여 만족스럽습니다.)"
|
118 |
|
119 |
current_step = plan.steps[step_index]
|
120 |
+
print(f"[{self.npc.korean_name}의 행동 계획 실행] {step_index + 1}/{len(plan.steps)}: {current_step}")
|
121 |
|
122 |
# 다음 단계로
|
123 |
plan.current_step_index += 1
|
npc_social_network/npc/npc_relationship.py
CHANGED
@@ -5,6 +5,7 @@ from typing import List, Optional, TYPE_CHECKING
|
|
5 |
if TYPE_CHECKING:
|
6 |
from .npc_memory import Memory
|
7 |
from .npc_base import NPC
|
|
|
8 |
|
9 |
class SocialProfile:
|
10 |
"""특정 대상과의 사회적 관계를 종합적으로 관리하는 클래스"""
|
@@ -78,7 +79,7 @@ class RelationshipManager:
|
|
78 |
profile = self._get_or_create_profile(target_name)
|
79 |
return profile.summary
|
80 |
|
81 |
-
def summarize_relationship(self, target_name: str):
|
82 |
""" LLM을 사용하여 특정 대상과의 관계를 주기적으로 요약하고 업데이트"""
|
83 |
from ..models.llm_helper import query_llm_with_prompt
|
84 |
profile = self._get_or_create_profile(target_name)
|
@@ -86,21 +87,24 @@ class RelationshipManager:
|
|
86 |
if not profile.key_memories:
|
87 |
return # 요약할 기억이 없으면 실행 안함
|
88 |
|
|
|
|
|
|
|
89 |
memory_details = "\n".join([f"- {mem.content} (감정: {mem.emotion})" for mem in profile.key_memories])
|
90 |
prompt = f"""
|
91 |
# 지시사항
|
92 |
-
당신은 '{self.owner_npc.
|
93 |
|
94 |
-
# '{
|
95 |
{memory_details}
|
96 |
|
97 |
-
→ '{
|
98 |
"""
|
99 |
|
100 |
summary = query_llm_with_prompt(prompt).replace("'", "").strip()
|
101 |
|
102 |
if summary and "[LLM Error]" not in summary:
|
103 |
profile.summary = summary
|
104 |
-
print(f"[{self.owner_npc.
|
105 |
|
106 |
|
|
|
5 |
if TYPE_CHECKING:
|
6 |
from .npc_memory import Memory
|
7 |
from .npc_base import NPC
|
8 |
+
from .npc_manager import NPCManager
|
9 |
|
10 |
class SocialProfile:
|
11 |
"""특정 대상과의 사회적 관계를 종합적으로 관리하는 클래스"""
|
|
|
79 |
profile = self._get_or_create_profile(target_name)
|
80 |
return profile.summary
|
81 |
|
82 |
+
def summarize_relationship(self, target_name: str, npc_manager: "NPCManager"):
|
83 |
""" LLM을 사용하여 특정 대상과의 관계를 주기적으로 요약하고 업데이트"""
|
84 |
from ..models.llm_helper import query_llm_with_prompt
|
85 |
profile = self._get_or_create_profile(target_name)
|
|
|
87 |
if not profile.key_memories:
|
88 |
return # 요약할 기억이 없으면 실행 안함
|
89 |
|
90 |
+
target_npc = npc_manager.get_npc_by_korean_name(target_name)
|
91 |
+
target_korean_name = target_npc.korean_name if target_npc else target_name
|
92 |
+
|
93 |
memory_details = "\n".join([f"- {mem.content} (감정: {mem.emotion})" for mem in profile.key_memories])
|
94 |
prompt = f"""
|
95 |
# 지시사항
|
96 |
+
당신은 '{self.owner_npc.korean_name}'입니다. 당신의 기억을 바탕으로 '{target_korean_name}'에 대한 당신의 생각과 감정을 한두 문장으로 솔직하게 요약해주세요.
|
97 |
|
98 |
+
# '{target_korean_name}'와(과)의 핵심 기억들
|
99 |
{memory_details}
|
100 |
|
101 |
+
→ '{target_korean_name}'에 대한 나의 생각:
|
102 |
"""
|
103 |
|
104 |
summary = query_llm_with_prompt(prompt).replace("'", "").strip()
|
105 |
|
106 |
if summary and "[LLM Error]" not in summary:
|
107 |
profile.summary = summary
|
108 |
+
print(f"[{self.owner_npc.korean_name}의 관계 요약 업데이트] → {target_korean_name}: {profile.summary}")
|
109 |
|
110 |
|
npc_social_network/routes/npc_route.py
CHANGED
@@ -13,15 +13,6 @@ npc_bp = Blueprint(
|
|
13 |
static_folder="../static"
|
14 |
)
|
15 |
|
16 |
-
# --- 한글 이름을 안전한 영어 파일명/ID로 변환하는 함수 ---
|
17 |
-
def to_english_id(name):
|
18 |
-
name_map = {
|
19 |
-
"엘린": "elin", "밥": "bob", "앨리스": "alice",
|
20 |
-
"찰리": "charlie", "다이애나": "diana"
|
21 |
-
}
|
22 |
-
return name_map.get(name, name.lower().replace(" ", "_"))
|
23 |
-
|
24 |
-
|
25 |
@npc_bp.route("/")
|
26 |
def dashboard():
|
27 |
return render_template("dashboard.html")
|
@@ -38,12 +29,12 @@ def get_world_state():
|
|
38 |
nodes = []
|
39 |
for npc in all_npcs:
|
40 |
# 각 NPC의 영어 ID를 기반으로 이미지 파일명을 생성합니다. (예: elin -> npc_elin.png)
|
41 |
-
image_filename = f"images/npc/npc_{
|
42 |
|
43 |
nodes.append({
|
44 |
# id는 영어로, label은 한글로 분리
|
45 |
-
"id":
|
46 |
-
"label": npc.
|
47 |
"shape": "image", # 노드 모양을 'image'로 지정합니다.
|
48 |
"image": url_for('npc_social.static', filename=image_filename), # 올바른 이미지 경로를 생성합니다.
|
49 |
"size": 40 # 이미지 크기를 적절히 조절합니다.
|
@@ -53,8 +44,8 @@ def get_world_state():
|
|
53 |
drawn_relations = set()
|
54 |
for npc in all_npcs:
|
55 |
for target_name, profile in npc.relationships.relationships.items():
|
56 |
-
from_id =
|
57 |
-
to_id =
|
58 |
|
59 |
relation_pair = tuple(sorted((from_id, to_id)))
|
60 |
if relation_pair in drawn_relations: continue
|
@@ -100,7 +91,7 @@ def inject_event():
|
|
100 |
npc_name, event_text = data.get("npc_name"), data.get("event_text")
|
101 |
|
102 |
with simulation_core.simulation_lock:
|
103 |
-
npc = simulation_core.npc_manager.
|
104 |
if npc and event_text:
|
105 |
npc.remember(content=f"[요약된 기억] {event_text}", importance=10, emotion="surprise", memory_type="Summary") # 수정 필요: 신이 돼서 넣는게 아니라, NPC가 마치 과거에 겪은 일처럼 자연스럽게 느끼도록
|
106 |
simulation_core.add_log(f"이벤트 주입(기억 요약) -> {npc_name}: '{event_text}'")
|
@@ -111,12 +102,12 @@ def inject_event():
|
|
111 |
@npc_bp.route("/api/npc_details/<npc_name>", methods=['GET'])
|
112 |
def get_npc_details(npc_name):
|
113 |
with simulation_core.simulation_lock:
|
114 |
-
npc = simulation_core.npc_manager.
|
115 |
if not npc:
|
116 |
return jsonify({"error": "NPC not found"}), 404
|
117 |
|
118 |
details = {
|
119 |
-
"name": npc.
|
120 |
"personality_summary": npc.personality.get_personality_summary(),
|
121 |
"emotions": npc.get_composite_emotion_state(top_n=5),
|
122 |
"goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "특별한 목표 없음",
|
|
|
13 |
static_folder="../static"
|
14 |
)
|
15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
@npc_bp.route("/")
|
17 |
def dashboard():
|
18 |
return render_template("dashboard.html")
|
|
|
29 |
nodes = []
|
30 |
for npc in all_npcs:
|
31 |
# 각 NPC의 영어 ID를 기반으로 이미지 파일명을 생성합니다. (예: elin -> npc_elin.png)
|
32 |
+
image_filename = f"images/npc/npc_{npc.name}.png"
|
33 |
|
34 |
nodes.append({
|
35 |
# id는 영어로, label은 한글로 분리
|
36 |
+
"id": npc.name,
|
37 |
+
"label": npc.korean_name,
|
38 |
"shape": "image", # 노드 모양을 'image'로 지정합니다.
|
39 |
"image": url_for('npc_social.static', filename=image_filename), # 올바른 이미지 경로를 생성합니다.
|
40 |
"size": 40 # 이미지 크기를 적절히 조절합니다.
|
|
|
44 |
drawn_relations = set()
|
45 |
for npc in all_npcs:
|
46 |
for target_name, profile in npc.relationships.relationships.items():
|
47 |
+
from_id = npc.name
|
48 |
+
to_id = target_name
|
49 |
|
50 |
relation_pair = tuple(sorted((from_id, to_id)))
|
51 |
if relation_pair in drawn_relations: continue
|
|
|
91 |
npc_name, event_text = data.get("npc_name"), data.get("event_text")
|
92 |
|
93 |
with simulation_core.simulation_lock:
|
94 |
+
npc = simulation_core.npc_manager.get_npc_by_korean_name(npc_name)
|
95 |
if npc and event_text:
|
96 |
npc.remember(content=f"[요약된 기억] {event_text}", importance=10, emotion="surprise", memory_type="Summary") # 수정 필요: 신이 돼서 넣는게 아니라, NPC가 마치 과거에 겪은 일처럼 자연스럽게 느끼도록
|
97 |
simulation_core.add_log(f"이벤트 주입(기억 요약) -> {npc_name}: '{event_text}'")
|
|
|
102 |
@npc_bp.route("/api/npc_details/<npc_name>", methods=['GET'])
|
103 |
def get_npc_details(npc_name):
|
104 |
with simulation_core.simulation_lock:
|
105 |
+
npc = simulation_core.npc_manager.get_npc_by_korean_name(npc_name)
|
106 |
if not npc:
|
107 |
return jsonify({"error": "NPC not found"}), 404
|
108 |
|
109 |
details = {
|
110 |
+
"name": npc.korean_name, "age": npc.age, "job": npc.job,
|
111 |
"personality_summary": npc.personality.get_personality_summary(),
|
112 |
"emotions": npc.get_composite_emotion_state(top_n=5),
|
113 |
"goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "특별한 목표 없음",
|
npc_social_network/scenarios/scenario_setup.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
from ..npc.npc_manager import NPCManager
|
2 |
from ..npc.npc_base import NPC
|
3 |
-
from ..npc.
|
4 |
|
5 |
def setup_initial_scenario() -> NPCManager:
|
6 |
"""
|
@@ -15,23 +15,23 @@ def setup_initial_scenario() -> NPCManager:
|
|
15 |
# --- 1. NPC 객체 생성 ---
|
16 |
# 엘린: 호기심 많고 감정적인 마법사
|
17 |
personality_elin = {"sensitive": 0.8, "stoic": 0.3, "cognitive_bias": 0.7}
|
18 |
-
elin = NPC("엘린", "마법사", personality=personality_elin)
|
19 |
|
20 |
# 밥: 무뚝뚝하지만 정직한 대장장이
|
21 |
personality_bob = {"sensitive": 0.2, "stoic": 0.8, "cognitive_bias": 0.4}
|
22 |
-
bob = NPC("밥", "대장장이", personality=personality_bob)
|
23 |
|
24 |
# 앨리스: 사교적이고 계산적인 상인
|
25 |
personality_alice = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.8}
|
26 |
-
alice = NPC("앨리스", "상인", personality=personality_alice)
|
27 |
|
28 |
# 찰리: 성실하고 평화로운 농부
|
29 |
personality_charlie = {"sensitive": 0.6, "stoic": 0.6, "cognitive_bias": 0.3}
|
30 |
-
charlie = NPC("찰리", "농부", personality=personality_charlie)
|
31 |
|
32 |
# 다이애나: 조용하고 관찰력 있는 사서
|
33 |
personality_diana = {"sensitive": 0.7, "stoic": 0.7, "cognitive_bias": 0.9}
|
34 |
-
diana = NPC("다이애나", "사서", personality=personality_diana)
|
35 |
|
36 |
# --- 2. 초기 기억 주입 ---
|
37 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
@@ -49,15 +49,20 @@ def setup_initial_scenario() -> NPCManager:
|
|
49 |
|
50 |
# --- 4. 초기 관계 설정 ---
|
51 |
# 엘린 <-> 앨리스 (나쁜 관계)
|
52 |
-
elin.relationships.update_relationship("
|
53 |
-
alice.relationships.update_relationship("
|
54 |
|
55 |
# 밥 <-> 찰리 (좋은 관계)
|
56 |
-
bob.relationships.update_relationship("
|
57 |
-
charlie.relationships.update_relationship("
|
58 |
|
59 |
# 다이애나 -> 밥 (약간 부정적)
|
60 |
-
diana.relationships.update_relationship("
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
print("✅ '시작의 마을' 시나리오 설정이 완료되었습니다.")
|
63 |
return npc_manager
|
|
|
1 |
from ..npc.npc_manager import NPCManager
|
2 |
from ..npc.npc_base import NPC
|
3 |
+
from ..npc.npc_memory_embedder import embed_npc_memories
|
4 |
|
5 |
def setup_initial_scenario() -> NPCManager:
|
6 |
"""
|
|
|
15 |
# --- 1. NPC 객체 생성 ---
|
16 |
# 엘린: 호기심 많고 감정적인 마법사
|
17 |
personality_elin = {"sensitive": 0.8, "stoic": 0.3, "cognitive_bias": 0.7}
|
18 |
+
elin = NPC('elin', "엘린", "마법사", personality=personality_elin)
|
19 |
|
20 |
# 밥: 무뚝뚝하지만 정직한 대장장이
|
21 |
personality_bob = {"sensitive": 0.2, "stoic": 0.8, "cognitive_bias": 0.4}
|
22 |
+
bob = NPC('bob', "밥", "대장장이", personality=personality_bob)
|
23 |
|
24 |
# 앨리스: 사교적이고 계산적인 상인
|
25 |
personality_alice = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.8}
|
26 |
+
alice = NPC('alice', "앨리스", "상인", personality=personality_alice)
|
27 |
|
28 |
# 찰리: 성실하고 평화로운 농부
|
29 |
personality_charlie = {"sensitive": 0.6, "stoic": 0.6, "cognitive_bias": 0.3}
|
30 |
+
charlie = NPC('charlie', "찰리", "농부", personality=personality_charlie)
|
31 |
|
32 |
# 다이애나: 조용하고 관찰력 있는 사서
|
33 |
personality_diana = {"sensitive": 0.7, "stoic": 0.7, "cognitive_bias": 0.9}
|
34 |
+
diana = NPC('diana', "다이애나", "사서", personality=personality_diana)
|
35 |
|
36 |
# --- 2. 초기 기억 주입 ---
|
37 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
|
|
49 |
|
50 |
# --- 4. 초기 관계 설정 ---
|
51 |
# 엘린 <-> 앨리스 (나쁜 관계)
|
52 |
+
elin.relationships.update_relationship("alice", "anger", strength=5.0)
|
53 |
+
alice.relationships.update_relationship("elin", "resentment", strength=4.0)
|
54 |
|
55 |
# 밥 <-> 찰리 (좋은 관계)
|
56 |
+
bob.relationships.update_relationship("charlie", "gratitude", strength=6.0)
|
57 |
+
charlie.relationships.update_relationship("bob", "joy", strength=7.0)
|
58 |
|
59 |
# 다이애나 -> 밥 (약간 부정적)
|
60 |
+
diana.relationships.update_relationship("bob", "disgust", strength=3.0)
|
61 |
+
|
62 |
+
# 모든 NPC의 초기 기억을 FAISS 인덱스로 저장
|
63 |
+
print("모든 NPC의 초기 기억에 대한 FAISS 인덱스를 생성합니다...")
|
64 |
+
for npc in npc_manager.all():
|
65 |
+
embed_npc_memories(npc)
|
66 |
|
67 |
print("✅ '시작의 마을' 시나리오 설정이 완료되었습니다.")
|
68 |
return npc_manager
|
npc_social_network/simulation_core.py
CHANGED
@@ -30,16 +30,48 @@ def add_log(message):
|
|
30 |
|
31 |
def tick_simulation():
|
32 |
"""시뮬레이션의 시간을 한 단계 진행"""
|
33 |
-
if not npc_manager
|
34 |
return
|
35 |
|
36 |
with simulation_lock:
|
|
|
|
|
|
|
37 |
add_log("시뮬레이션 틱 시작")
|
38 |
|
39 |
for npc in npc_manager.all():
|
40 |
npc.decay_emotions()
|
41 |
npc.decay_memories()
|
42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
if len(npc_manager.all()) >= 2:
|
45 |
try:
|
|
|
30 |
|
31 |
def tick_simulation():
|
32 |
"""시뮬레이션의 시간을 한 단계 진행"""
|
33 |
+
if not npc_manager:
|
34 |
return
|
35 |
|
36 |
with simulation_lock:
|
37 |
+
if simulation_paused: # 정지 상태에서는 틱 실행 x
|
38 |
+
return
|
39 |
+
|
40 |
add_log("시뮬레이션 틱 시작")
|
41 |
|
42 |
for npc in npc_manager.all():
|
43 |
npc.decay_emotions()
|
44 |
npc.decay_memories()
|
45 |
+
if random.random() < 0.1:
|
46 |
+
npc.update_autonomous_behavior("자율 행동 시간", npc_manager, add_log)
|
47 |
+
|
48 |
+
if len(npc_manager.all()) >= 3: # 목격자가 있으려면 최소 3명 필요
|
49 |
+
try:
|
50 |
+
add_log("NPC 간 자율 상호작용 및 목격 시도")
|
51 |
+
# 1. 상호작용을 호출하고 참여자들을 반환받음
|
52 |
+
initiator, target, initial_utterance = npc_manager.initiate_npc_to_npc_interaction("자율 행동 시간")
|
53 |
+
|
54 |
+
# 2. 상호작용이 성공했고, 목격자가 될 NPC가 있다면
|
55 |
+
if initiator and target:
|
56 |
+
potential_witnesses = [npc for npc in npc_manager.all() if npc not in [initiator, target]]
|
57 |
+
if potential_witnesses:
|
58 |
+
witness = random.choice(potential_witnesses)
|
59 |
+
|
60 |
+
# 3. 목격한 내용을 바탕으로 '소문' 기억 생성 (대화 내용 요약, 관계에 따른 거짓말 등 고도화 가능)
|
61 |
+
gossip_content = f"'{initiator.name}'이(가) '{target.name}'에게 '{initial_utterance[:100]}...'라고 말하는 것을 봤다."
|
62 |
+
|
63 |
+
# 4. 목격자가 '소문'을 기억하도록 함
|
64 |
+
witness.remember(
|
65 |
+
content=gossip_content,
|
66 |
+
importance=4,
|
67 |
+
emotion="curiosity",
|
68 |
+
memory_type="Gossip",
|
69 |
+
npc_manager = npc_manager
|
70 |
+
)
|
71 |
+
add_log(f"[목격] {witness.name}이(가) {initiator.name}와 {target.name}의 대화를 목격함.")
|
72 |
+
|
73 |
+
except Exception as e:
|
74 |
+
add_log(f"상호작용/목격 중 오류 발생: {e}")
|
75 |
|
76 |
if len(npc_manager.all()) >= 2:
|
77 |
try:
|