humanda5 commited on
Commit
a147abc
·
1 Parent(s): 1a6fb12

NPC 시뮬레이션 완성

Browse files
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
- You are an AI assistant for an NPC named '{npc_name}'.
195
- The NPC's goal is: "{goal}"
196
- create a 3 to 5 step action plan to achieve this goal.
197
- Your response MUST be a valid JSON array of strings, where each string is a step in the plan.
 
198
 
199
- # Plan (JSON array of strings only):
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
- # Reputation Information
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.all() if npc.name != self.name]
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
- # [✨ 수정] 2. '소문 전파'라는 행동에 대한 명확한 기억을 별도로 생성
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", "플레이어", "관찰자", personality=player_personality)
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: #fff3cd; 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; }
 
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; }