Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
a147abc
1
Parent(s):
1a6fb12
NPC 시뮬레이션 완성
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/elin.faiss +0 -0
- npc_social_network/data/vectorstores/player.faiss +0 -0
- npc_social_network/models/llm_helper.py +25 -10
- npc_social_network/npc/npc_base.py +17 -3
- npc_social_network/scenarios/scenario_setup.py +10 -1
- npc_social_network/templates/dashboard.html +1 -1
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
CHANGED
Binary files a/npc_social_network/data/vectorstores/alice.faiss and b/npc_social_network/data/vectorstores/alice.faiss differ
|
|
npc_social_network/data/vectorstores/elin.faiss
CHANGED
Binary files a/npc_social_network/data/vectorstores/elin.faiss and b/npc_social_network/data/vectorstores/elin.faiss differ
|
|
npc_social_network/data/vectorstores/player.faiss
CHANGED
Binary files a/npc_social_network/data/vectorstores/player.faiss and b/npc_social_network/data/vectorstores/player.faiss differ
|
|
npc_social_network/models/llm_helper.py
CHANGED
@@ -162,6 +162,9 @@ def _query_llm_for_json_robustly(prompt: str) -> dict | list:
|
|
162 |
text_to_parse = json_match.group(0)
|
163 |
else:
|
164 |
text_to_parse = response_text # 최후의 경우 원본 텍스트 사용
|
|
|
|
|
|
|
165 |
|
166 |
# 추출된 텍스트로 파싱 및 자가 교정 시도
|
167 |
try:
|
@@ -191,12 +194,13 @@ def generate_plan_from_goal(npc_name: str, goal: str) -> List[str]:
|
|
191 |
"""목표가 주어지면, 가디언 함수를 이용해 안정적으로 계획을 생성"""
|
192 |
prompt = f"""
|
193 |
# Instruction
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
|
|
198 |
|
199 |
-
#
|
200 |
"""
|
201 |
result = _query_llm_for_json_robustly(prompt)
|
202 |
if isinstance(result, list) and all(isinstance(s, str) for s in result):
|
@@ -226,6 +230,17 @@ def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conve
|
|
226 |
# 프롬프트에 예쁘게 들어갈 수 있도록 앞뒤로 줄바꿈 추가
|
227 |
symbolic_section = f"\n{symbolic_section_title}\n{symbolic_contents}"
|
228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
if is_final_turn:
|
230 |
task_instruction = """
|
231 |
- 상대방이 대화를 마무리하려고 합니다. 상대방의 마지막 말에 어울리는 자연스러운 작별 인사를 생성하세요. (예: "네, 조심히 가세요.", "다음에 또 봬요.", "알겠습니다. 좋은 하루 보내세요.")
|
@@ -251,9 +266,8 @@ def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conve
|
|
251 |
|
252 |
# Inner State
|
253 |
- "{target.korean_name}"와의 관계: {speaker.relationships.get_relationship_summary(target.name)}
|
254 |
-
|
255 |
-
|
256 |
-
{reputation_info}
|
257 |
|
258 |
{symbolic_section}
|
259 |
|
@@ -269,6 +283,7 @@ def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conve
|
|
269 |
{task_instruction}
|
270 |
- 당신의 응답은 반드시 아래 JSON 형식이어야 합니다.
|
271 |
- 대사에는 이름, 행동 묘사, 따옴표를 절대 포함하지 마세요.
|
|
|
272 |
- 아래의 'Conversation Strategies'과 같이 현재 대화의 맥락과 당신의 성격에 가장 적절한 행동 하나를 선택하세요.
|
273 |
|
274 |
# Conversation Strategies
|
@@ -353,11 +368,11 @@ def evaluate_goal_achievement(initiator_name: str, goal: str, conversation_trans
|
|
353 |
결과는 반드시 아래 JSON 형식이어야 합니다.
|
354 |
|
355 |
# Evaluation (JSON format only)
|
356 |
-
{
|
357 |
"goal_achieved": True,
|
358 |
"reason": "목표 달성 또는 실패에 대한 간결한 이유 (예: '상대방을 위로하고 지지하며 긍정적인 반응을 이끌어냈다.)",
|
359 |
"initiator_emotion": "이 결과를 얻은 후 대화 시작자가 느낄 감정 (아래 'Emotion_List에서 찾아서 한 단어로 표현)
|
360 |
-
}
|
361 |
|
362 |
# Emotion_List
|
363 |
{EMOTION_LIST}
|
|
|
162 |
text_to_parse = json_match.group(0)
|
163 |
else:
|
164 |
text_to_parse = response_text # 최후의 경우 원본 텍스트 사용
|
165 |
+
|
166 |
+
# 딕셔너리나 리스트의 마지막 요소 뒤에 붙은 꼬리표 쉼표 제거
|
167 |
+
text_to_parse = re.sub(r',\s*(\}|\])', r'\1', text_to_parse)
|
168 |
|
169 |
# 추출된 텍스트로 파싱 및 자가 교정 시도
|
170 |
try:
|
|
|
194 |
"""목표가 주어지면, 가디언 함수를 이용해 안정적으로 계획을 생성"""
|
195 |
prompt = f"""
|
196 |
# Instruction
|
197 |
+
- 당신은 '{npc_name}'이라는 NPC를 위한 AI 조수입니다.
|
198 |
+
- 이 NPC의 목표는 "{goal}" 입니다.
|
199 |
+
- 이 목표를 달성하기 위한 3~5단계의 구체적인 행동 계획을 만들어주세요.
|
200 |
+
- 당신의 응답은 반드시 각 단계가 문자열로 이루어진 유효한 JSON 배열이어야 합니다.
|
201 |
+
- 행동 계획에 대한 내용만 작성해주세요. ('1단계'와 같은 불필요한 단어는 필요없습니다.)
|
202 |
|
203 |
+
# 계획 (문자열로 구성된 JSON 배열 형식):
|
204 |
"""
|
205 |
result = _query_llm_for_json_robustly(prompt)
|
206 |
if isinstance(result, list) and all(isinstance(s, str) for s in result):
|
|
|
230 |
# 프롬프트에 예쁘게 들어갈 수 있도록 앞뒤로 줄바꿈 추가
|
231 |
symbolic_section = f"\n{symbolic_section_title}\n{symbolic_contents}"
|
232 |
|
233 |
+
# 상대방에 대해 알고 있는 정보를 프롬프트에 추가
|
234 |
+
known_info_summary = "아직 아는 정보가 없음"
|
235 |
+
if target.name in speaker.knowledge:
|
236 |
+
known_facts = []
|
237 |
+
if "job" in speaker.knowledge[target.name]:
|
238 |
+
known_facts.append(f"직업은 {speaker.knowledge[target.name]['job']}이다.")
|
239 |
+
if "age" in speaker.knowledge[target.name]:
|
240 |
+
known_facts.append(f"나이는 {speaker.knowledge[target.name]['age']}이다.")
|
241 |
+
if known_facts:
|
242 |
+
known_info_summary = ", ".join(known_facts)
|
243 |
+
|
244 |
if is_final_turn:
|
245 |
task_instruction = """
|
246 |
- 상대방이 대화를 마무리하려고 합니다. 상대방의 마지막 말에 어울리는 자연스러운 작별 인사를 생성하세요. (예: "네, 조심히 가세요.", "다음에 또 봬요.", "알겠습니다. 좋은 하루 보내세요.")
|
|
|
266 |
|
267 |
# Inner State
|
268 |
- "{target.korean_name}"와의 관계: {speaker.relationships.get_relationship_summary(target.name)}
|
269 |
+
- {reputation_info}
|
270 |
+
- "{target.korean_name}"에 대해 내가 아는 정보: {known_info_summary}
|
|
|
271 |
|
272 |
{symbolic_section}
|
273 |
|
|
|
283 |
{task_instruction}
|
284 |
- 당신의 응답은 반드시 아래 JSON 형식이어야 합니다.
|
285 |
- 대사에는 이름, 행동 묘사, 따옴표를 절대 포함하지 마세요.
|
286 |
+
- 'Relevant Memories'이 현재 대화 상대 ('{target.korean_name}')와 관련 없는 내용일 수 있습니다. 다른 사람에 대한 기억을 현재 대화 상태에게 일어난 일인 것처럼 착각해서 말하지 마세요.
|
287 |
- 아래의 'Conversation Strategies'과 같이 현재 대화의 맥락과 당신의 성격에 가장 적절한 행동 하나를 선택하세요.
|
288 |
|
289 |
# Conversation Strategies
|
|
|
368 |
결과는 반드시 아래 JSON 형식이어야 합니다.
|
369 |
|
370 |
# Evaluation (JSON format only)
|
371 |
+
{
|
372 |
"goal_achieved": True,
|
373 |
"reason": "목표 달성 또는 실패에 대한 간결한 이유 (예: '상대방을 위로하고 지지하며 긍정적인 반응을 이끌어냈다.)",
|
374 |
"initiator_emotion": "이 결과를 얻은 후 대화 시작자가 느낄 감정 (아래 'Emotion_List에서 찾아서 한 단어로 표현)
|
375 |
+
}
|
376 |
|
377 |
# Emotion_List
|
378 |
{EMOTION_LIST}
|
npc_social_network/npc/npc_base.py
CHANGED
@@ -94,6 +94,7 @@ class NPC:
|
|
94 |
self.relationships = RelationshipManager(self)
|
95 |
self.reputation = {} # 다른 NPC에 대한 평판 저장
|
96 |
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
|
|
|
97 |
# 행동 관리자
|
98 |
self.behavior = BehaviorManager()
|
99 |
self.planner = PlannerManager(self)
|
@@ -376,6 +377,10 @@ class NPC:
|
|
376 |
NPC가 스스로 판단하여 행동을 결정하는 자율 행동 로직
|
377 |
주기적으로 호출되어 목표 설정, 계획 실행 등을 담당.
|
378 |
"""
|
|
|
|
|
|
|
|
|
379 |
# 1. 계획 실행 로직을 NPC가 직접 처리
|
380 |
if self.planner.has_active_plan():
|
381 |
step = self.planner.get_current_step()
|
@@ -389,6 +394,15 @@ class NPC:
|
|
389 |
action_type = plan_details.get("action_type")
|
390 |
target_korean_name = plan_details.get("target")
|
391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
392 |
if action_type == "TALK" and target_korean_name:
|
393 |
target_npc = self.manager.get_npc_by_korean_name(target_korean_name)
|
394 |
if target_npc and target_npc != self:
|
@@ -425,7 +439,7 @@ class NPC:
|
|
425 |
)
|
426 |
# 현재 단계 완료 처리
|
427 |
self.planner.complete_step()
|
428 |
-
|
429 |
# 2. 계획이 없으면 새로운 목표를 생성하도록 시도
|
430 |
else:
|
431 |
# 소문을 퍼뜨리려는 시도
|
@@ -434,7 +448,7 @@ class NPC:
|
|
434 |
gossip_to_spread = random.choice(gossip_memories)
|
435 |
|
436 |
# 아직 대화한 적 없는 NPC를 타겟으로 선정
|
437 |
-
potential_targets = [npc for npc in self.manager.
|
438 |
if potential_targets:
|
439 |
target_npc = random.choice(potential_targets)
|
440 |
|
@@ -450,7 +464,7 @@ class NPC:
|
|
450 |
simulation_core.add_log(f"[소문 전파] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
|
451 |
gossip_to_spread.is_shared = True
|
452 |
|
453 |
-
#
|
454 |
self.remember(
|
455 |
content=f"{target_npc.korean_name}에게 '{gossip_to_spread.content}'라는 소문에 대해 이야기했다.",
|
456 |
importance=5,
|
|
|
94 |
self.relationships = RelationshipManager(self)
|
95 |
self.reputation = {} # 다른 NPC에 대한 평판 저장
|
96 |
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
|
97 |
+
self.knowledge: dict[str, dict] = {} # 다른 NPC에 대한 사실 정보를 저장하는 '지식 베이스'
|
98 |
# 행동 관리자
|
99 |
self.behavior = BehaviorManager()
|
100 |
self.planner = PlannerManager(self)
|
|
|
377 |
NPC가 스스로 판단하여 행동을 결정하는 자율 행동 로직
|
378 |
주기적으로 호출되어 목표 설정, 계획 실행 등을 담당.
|
379 |
"""
|
380 |
+
# 플레이어가 비활성화 상태일 경우, 자율 행동을 실행하지 않도록 하는 안전장치
|
381 |
+
if self.name == "player" and not self.manager.player_is_active:
|
382 |
+
return
|
383 |
+
|
384 |
# 1. 계획 실행 로직을 NPC가 직접 처리
|
385 |
if self.planner.has_active_plan():
|
386 |
step = self.planner.get_current_step()
|
|
|
394 |
action_type = plan_details.get("action_type")
|
395 |
target_korean_name = plan_details.get("target")
|
396 |
|
397 |
+
# 행동 실행 전 '조건 확인'단계
|
398 |
+
can_execute = True
|
399 |
+
if target_korean_name == "플레이어" and not self.manager.player_is_active:
|
400 |
+
# 목표 대상이 '플레이어'인데, 플레이어가 비활성화 상태일 경우
|
401 |
+
can_execute = False
|
402 |
+
simulation_core.add_log(f"[{self.korean_name}의 생각] 지금은 플레이어가 없으니, '{step}' 계획은 나중에 다시 시도해야겠다.")
|
403 |
+
return
|
404 |
+
|
405 |
+
# 행동 실행
|
406 |
if action_type == "TALK" and target_korean_name:
|
407 |
target_npc = self.manager.get_npc_by_korean_name(target_korean_name)
|
408 |
if target_npc and target_npc != self:
|
|
|
439 |
)
|
440 |
# 현재 단계 완료 처리
|
441 |
self.planner.complete_step()
|
442 |
+
|
443 |
# 2. 계획이 없으면 새로운 목표를 생성하도록 시도
|
444 |
else:
|
445 |
# 소문을 퍼뜨리려는 시도
|
|
|
448 |
gossip_to_spread = random.choice(gossip_memories)
|
449 |
|
450 |
# 아직 대화한 적 없는 NPC를 타겟으로 선정
|
451 |
+
potential_targets = [npc for npc in self.manager.get_interactive_npcs() if npc.name != self.name]
|
452 |
if potential_targets:
|
453 |
target_npc = random.choice(potential_targets)
|
454 |
|
|
|
464 |
simulation_core.add_log(f"[소문 전파] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
|
465 |
gossip_to_spread.is_shared = True
|
466 |
|
467 |
+
# 2. '소문 전파'라는 행동에 대한 명확한 기억을 별도로 생성
|
468 |
self.remember(
|
469 |
content=f"{target_npc.korean_name}에게 '{gossip_to_spread.content}'라는 소문에 대해 이야기했다.",
|
470 |
importance=5,
|
npc_social_network/scenarios/scenario_setup.py
CHANGED
@@ -36,7 +36,7 @@ def setup_initial_scenario() -> NPCManager:
|
|
36 |
# 플레이어를 위한 NPC 객체를 생성
|
37 |
# 플레이어는 고유 ID "Player"를 가지며, 성격은 가장 균형잡힌 상태로 시작
|
38 |
player_personality = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.5}
|
39 |
-
player = NPC("player", "플레이어", "
|
40 |
|
41 |
# --- 2. 초기 기억 주입 ---
|
42 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
@@ -69,6 +69,15 @@ def setup_initial_scenario() -> NPCManager:
|
|
69 |
elin.relationships.update_relationship("player", "neutral", strength=0.0)
|
70 |
charlie.relationships.update_relationship("player", "neutral", strength=0.0)
|
71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
# 모든 NPC의 초기 기억을 FAISS 인덱스로 저장
|
73 |
print("모든 NPC의 초기 기억에 대한 FAISS 인덱스를 생성합니다...")
|
74 |
for npc in npc_manager.all():
|
|
|
36 |
# 플레이어를 위한 NPC 객체를 생성
|
37 |
# 플레이어는 고유 ID "Player"를 가지며, 성격은 가장 균형잡힌 상태로 시작
|
38 |
player_personality = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.5}
|
39 |
+
player = NPC("player", "플레이어", "모험가", personality=player_personality)
|
40 |
|
41 |
# --- 2. 초기 기억 주입 ---
|
42 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
|
|
69 |
elin.relationships.update_relationship("player", "neutral", strength=0.0)
|
70 |
charlie.relationships.update_relationship("player", "neutral", strength=0.0)
|
71 |
|
72 |
+
# --- 5. 초기 지식 베이스 설정 ---
|
73 |
+
all_npc = npc_manager.all()
|
74 |
+
for npc in all_npc:
|
75 |
+
for other_npc in all_npc:
|
76 |
+
if npc != other_npc:
|
77 |
+
npc.knowledge[other_npc.name] = { "job": other_npc.job, # 모든 NPC가 서로의 직업을 알고 시작하도록 설정
|
78 |
+
"age": other_npc.age # 모든 NPC가 서로의 나이를 알고 시작하도록 설정
|
79 |
+
}
|
80 |
+
|
81 |
# 모든 NPC의 초기 기억을 FAISS 인덱스로 저장
|
82 |
print("모든 NPC의 초기 기억에 대한 FAISS 인덱스를 생성합니다...")
|
83 |
for npc in npc_manager.all():
|
npc_social_network/templates/dashboard.html
CHANGED
@@ -14,7 +14,7 @@
|
|
14 |
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); min-height: 0; }
|
15 |
|
16 |
/* 플레이어 대화 */
|
17 |
-
#player-input-area { padding: 10px; background-color: #
|
18 |
#player-input-area p { margin: 0 0 10px 0; font-weight: 500; font-size: 14px; }
|
19 |
.player-input-wrapper { display: flex; gap: 10px }
|
20 |
#player-input-text { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; }
|
|
|
14 |
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); min-height: 0; }
|
15 |
|
16 |
/* 플레이어 대화 */
|
17 |
+
#player-input-area { padding: 10px; background-color: #eefc2a; border-top: 2px solid #ffeeba; }
|
18 |
#player-input-area p { margin: 0 0 10px 0; font-weight: 500; font-size: 14px; }
|
19 |
.player-input-wrapper { display: flex; gap: 10px }
|
20 |
#player-input-text { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; }
|