humanda5 commited on
Commit
b5ff045
·
1 Parent(s): 2fe1b55

정상 작동, 수정 완료

Browse files
app.py CHANGED
@@ -4,6 +4,7 @@ from flask import Flask, render_template, url_for, redirect
4
  # from npc_social_network import simulation_core
5
  # from stock.routes.stock_route import stock_bp
6
  import threading
 
7
 
8
  # -------------------------------------------------------------------
9
  # 파일 1: portfolio/app.py (수정)
@@ -15,6 +16,14 @@ import threading
15
  init_lock = threading.Lock()
16
  simulation_initialized = False
17
 
 
 
 
 
 
 
 
 
18
  def create_app():
19
  """Flask 앱을 생성하고, 필요한 Blueprint를 등록하는 팩토리 함수."""
20
  app = Flask(__name__,
@@ -33,8 +42,6 @@ def create_app():
33
  print("="*60)
34
  traceback.print_exc() # 에러의 전체 경로를 출력합니다.
35
  print("="*60)
36
- # 이 에러가 해결될 때까지 서버는 정상 작동하지 않을 수 있습니다.
37
- # --- FINAL DEBUGGING TOOL END ---
38
 
39
 
40
  # 포트폴리오의 메인 랜딩 페이지
@@ -62,6 +69,8 @@ if __name__ == '__main__':
62
  print("="*60)
63
 
64
  # 3. Flask 웹 서버 실행
 
 
65
  # use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
66
  # 시뮬레이션 스레드가 두 번 시작되지 않도록 합니다.
67
  app.run(debug=True, use_reloader=False, port=5000)
 
4
  # from npc_social_network import simulation_core
5
  # from stock.routes.stock_route import stock_bp
6
  import threading
7
+ import logging
8
 
9
  # -------------------------------------------------------------------
10
  # 파일 1: portfolio/app.py (수정)
 
16
  init_lock = threading.Lock()
17
  simulation_initialized = False
18
 
19
+ # 특정 로그 메시지를 걸러내기 위한 필터 클래스
20
+ class LogFilter(logging.Filter):
21
+ def filter(self, record):
22
+ # /api/world_state 경로에 대한 로그는 기록하지 않음
23
+ if "/api/world_state" in record.getMessage():
24
+ return False
25
+ return True
26
+
27
  def create_app():
28
  """Flask 앱을 생성하고, 필요한 Blueprint를 등록하는 팩토리 함수."""
29
  app = Flask(__name__,
 
42
  print("="*60)
43
  traceback.print_exc() # 에러의 전체 경로를 출력합니다.
44
  print("="*60)
 
 
45
 
46
 
47
  # 포트폴리오의 메인 랜딩 페이지
 
69
  print("="*60)
70
 
71
  # 3. Flask 웹 서버 실행
72
+ log = logging.getLogger('werkzeug')
73
+ log.addFilter(LogFilter())
74
  # use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
75
  # 시뮬레이션 스레드가 두 번 시작되지 않도록 합니다.
76
  app.run(debug=True, use_reloader=False, port=5000)
npc_social_network/README.txt CHANGED
@@ -233,4 +233,26 @@ Phase 5: 논문/연구 결과 정리 - 최종 과제
233
 
234
  ---
235
 
236
- © 2025 NPC Social Network Simulation Project v3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  ---
235
 
236
+ © 2025 NPC Social Network Simulation Project v3
237
+
238
+ ---
239
+ 3. 플레이어 개입 및 관계 형성 시스템
240
+ 목표: 플레이어가 '신'의 관점에서 시뮬레이션에 직접 개입하여 새로운 관계나 사건을 만들어내는 기능을 구현합니다.
241
+
242
+ 주요 기능:
243
+
244
+ 관계 강제 설정: 플레이어가 두 NPC를 선택하여 '친구'나 '라이벌' 같은 관계를 즉시 설정합니다.
245
+
246
+ 상황 연출 및 대화 시작: 장소, 시간, 주제 등을 설정하여 특정 NPC들 간의 만남과 대화를 강제로 시작시킬 수 있습니다. (예: "저녁에 선술집에서 밥과 앨리스가 돈 문제로 다투게 하라")
247
+
248
+ '소개' 기능: 플레이어가 A를 시켜, 서로 모르는 B와 C를 소개해주는 이벤트를 발생시킵니다.
249
+ ---
250
+ 5. UI/UX 안정성 및 편의성 개선
251
+ 목표: 대시보드의 사용성을 개선합니다.
252
+
253
+ 주요 기능:
254
+
255
+ 네트워크 안정화: 관계도 업데이트 시 그래프가 심하게 흔들리거나 돌아가는 문제를 해결합니다.
256
+
257
+ 레이아웃 정리: 화면 크기 변화에도 UI 요소들이 깨지지 않도록 CSS를 수정합니다.
258
+ ---
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/bob.faiss CHANGED
Binary files a/npc_social_network/data/vectorstores/bob.faiss and b/npc_social_network/data/vectorstores/bob.faiss differ
 
npc_social_network/data/vectorstores/charlie.faiss CHANGED
Binary files a/npc_social_network/data/vectorstores/charlie.faiss and b/npc_social_network/data/vectorstores/charlie.faiss differ
 
npc_social_network/data/vectorstores/diana.faiss CHANGED
Binary files a/npc_social_network/data/vectorstores/diana.faiss and b/npc_social_network/data/vectorstores/diana.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/models/llm_helper.py CHANGED
@@ -157,4 +157,20 @@ Do not include any other text, explanations, or markdown.
157
  except json.JSONDecodeError:
158
  # 4단계: 최종 실패 시, 안전한 기본값 반환
159
  print(f"LLM 자가 교정 실패. 원본 응답: {response_text}")
160
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  except json.JSONDecodeError:
158
  # 4단계: 최종 실패 시, 안전한 기본값 반환
159
  print(f"LLM 자가 교정 실패. 원본 응답: {response_text}")
160
+ return {}
161
+
162
+ def generate_plan_from_goal(npc_name: str, goal: str) -> List[str]:
163
+ """목표가 주어지면, 가디언 함수를 이용해 안정적으로 계획을 생성"""
164
+ prompt = f"""
165
+ # Instruction
166
+ You are an AI assistant for an NPC named '{npc_name}'.
167
+ The NPC's goal is: "{goal}"
168
+ create a 3 to 5 step action plan to achieve this goal.
169
+ Your response MUST be a valid JSON array of strings, where each string is a step in the plan.
170
+
171
+ # Plan (JSON array of strings only):
172
+ """
173
+ result = _query_llm_for_json_robustly(prompt)
174
+ if isinstance(result, list) and all(isinstance(s, str) for s in result):
175
+ return result
176
+ return ["[계획 생성에 실패했습니다]"] # 최종 실패 시 안전한 값 반환
npc_social_network/npc/npc_base.py CHANGED
@@ -489,7 +489,7 @@ class NPC:
489
  )
490
 
491
  if gossip_dialogue and "[LLM Error]" not in gossip_dialogue:
492
- print(f"[소문 전파] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
493
  gossip_to_spread.is_shared = True
494
 
495
  # [✨ 수정] 2. '소문 전파'라는 행동에 대한 명확한 기억을 별도로 생성
@@ -560,7 +560,7 @@ class NPC:
560
  emotion = "rumination", # 반추, 성찰과 관련된 감정
561
  memory_type="Symbolic",
562
  )
563
- print(f"[{self.korean_name}]의 새로운 깨달음: {symbolic_thought}")
564
 
565
  def update_reputation(self, target_name: str, score_change: int):
566
  """특정 대상에 대한 평판 점수를 변경"""
 
489
  )
490
 
491
  if gossip_dialogue and "[LLM Error]" not in gossip_dialogue:
492
+ simulation_core.add_log(f"[소문 전파] {self.korean_name} -> {target_npc.korean_name}: {gossip_dialogue}")
493
  gossip_to_spread.is_shared = True
494
 
495
  # [✨ 수정] 2. '소문 전파'라는 행동에 대한 명확한 기억을 별도로 생성
 
560
  emotion = "rumination", # 반추, 성찰과 관련된 감정
561
  memory_type="Symbolic",
562
  )
563
+ simulation_core.add_log(f"[{self.korean_name}]의 새로운 깨달음: {symbolic_thought}")
564
 
565
  def update_reputation(self, target_name: str, score_change: int):
566
  """특정 대상에 대한 평판 점수를 변경"""
npc_social_network/npc/npc_manager.py CHANGED
@@ -1,6 +1,7 @@
1
  # portfolio/npc_social_network/npc/npc_manager.py
2
  import random
3
  from typing import TYPE_CHECKING, List, Optional
 
4
 
5
  if TYPE_CHECKING:
6
  from .npc_base import NPC
@@ -67,7 +68,7 @@ class NPCManager:
67
  initiator_postposition = get_korean_postposition(initiator.korean_name, "이", "가")
68
  target_postposition = get_korean_postposition(target.korean_name, "에게", "에게")
69
 
70
- print(f"\n---[NPC 상호작용 이벤트]---\n{initiator.korean_name}{initiator_postposition} {target.korean_name}{target_postposition} 상호작용을 시도합니다.")
71
 
72
  if topic:
73
  # 주제가 있는 경우
@@ -106,30 +107,33 @@ class NPCManager:
106
  """
107
  initial_utterance = query_llm_with_prompt(prompt).strip()
108
 
109
- # LLM이 실수로 자신의 이름을 답변에 포함했을 경우, 이를 제거하는 후처리 로직
 
 
 
110
  name_prefix = f"{initiator.korean_name}:"
111
- if initial_utterance.startswith(name_prefix):
112
- initial_utterance = initial_utterance[len(name_prefix):].strip()
113
 
114
- # 따옴표도 한 번 더 제거
115
- initial_utterance = initial_utterance.strip('"')
116
 
117
- if "[LLM Error]" in initial_utterance:
 
118
  print(f"[{initiator.korean_name}] 대화 시작에 실패했습니다.")
119
  return None, None, None
120
 
121
- print(f"[{initiator.korean_name}]: {initial_utterance}")
122
 
123
  # 2. Target이 Initiator의 말을 듣고 응답 생성
124
  # generate_dialogue 함수를 재사용하되, target_npc 인자를 전달
125
  response_utterance = target.generate_dialogue(
126
- user_input=initial_utterance,
127
  time_context = time_context,
128
  target_npc=initiator
129
  )
130
 
131
- print(f"[{target.korean_name}]: {response_utterance}")
132
- print("-------------------------\n")
133
 
134
  # 3. 대화가 끝난 후, 목격자를 선정하여 소문을 생성합니다.
135
  potential_witnesses = [npc for npc in self.npcs if npc not in [initiator, target]]
@@ -150,7 +154,7 @@ class NPCManager:
150
  memory_type="Gossip",
151
  )
152
  # 로그는 simulation_core의 add_log를 직접 호출할 수 없으므로 print로 대체
153
- print(f"[목격] {witness.korean_name}{witness_postposition} {initiator.korean_name}와 {target.korean_name}의 대화를 목격함.")
154
 
155
- return initiator, target, initial_utterance
156
 
 
1
  # portfolio/npc_social_network/npc/npc_manager.py
2
  import random
3
  from typing import TYPE_CHECKING, List, Optional
4
+ from .. import simulation_core
5
 
6
  if TYPE_CHECKING:
7
  from .npc_base import NPC
 
68
  initiator_postposition = get_korean_postposition(initiator.korean_name, "이", "가")
69
  target_postposition = get_korean_postposition(target.korean_name, "에게", "에게")
70
 
71
+ simulation_core.add_log(f"\n---[NPC 상호작용 이벤트]---\n{initiator.korean_name}{initiator_postposition} {target.korean_name}{target_postposition} 상호작용을 시도합니다.")
72
 
73
  if topic:
74
  # 주제가 있는 경우
 
107
  """
108
  initial_utterance = query_llm_with_prompt(prompt).strip()
109
 
110
+ # 따옴표도 제거
111
+ clean_utterance = initial_utterance.strip('"')
112
+
113
+ # 이름 접두사가 있는지 확인하고 제거
114
  name_prefix = f"{initiator.korean_name}:"
115
+ if clean_utterance.startswith(name_prefix):
116
+ clean_utterance = clean_utterance[len(name_prefix):].strip()
117
 
118
+ # 따옴표도 제거
119
+ final_utterance = clean_utterance.strip().strip('"')
120
 
121
+ if "[LLM Error]" in final_utterance or not final_utterance:
122
+ simulation_core.add_log("[Error: {initiator.korean_name} 대화 시작 실패]")
123
  print(f"[{initiator.korean_name}] 대화 시작에 실패했습니다.")
124
  return None, None, None
125
 
126
+ simulation_core.add_log(f"[{initiator.korean_name}]: {final_utterance}")
127
 
128
  # 2. Target이 Initiator의 말을 듣고 응답 생성
129
  # generate_dialogue 함수를 재사용하되, target_npc 인자를 전달
130
  response_utterance = target.generate_dialogue(
131
+ user_input=final_utterance,
132
  time_context = time_context,
133
  target_npc=initiator
134
  )
135
 
136
+ simulation_core.add_log(f"[{target.korean_name}]: {response_utterance}")
 
137
 
138
  # 3. 대화가 끝난 후, 목격자를 선정하여 소문을 생성합니다.
139
  potential_witnesses = [npc for npc in self.npcs if npc not in [initiator, target]]
 
154
  memory_type="Gossip",
155
  )
156
  # 로그는 simulation_core의 add_log를 직접 호출할 수 없으므로 print로 대체
157
+ simulation_core.add_log(f"[목격] {witness.korean_name}{witness_postposition} {initiator.korean_name}와 {target.korean_name}의 대화를 목격함.")
158
 
159
+ return initiator, target, final_utterance
160
 
npc_social_network/npc/npc_planner.py CHANGED
@@ -1,7 +1,7 @@
1
  # portfolio/npc_social_network/npc/npc_planner.py
2
  from .. import simulation_core
3
  from typing import List, Optional, TYPE_CHECKING
4
- from ..models.llm_helper import query_llm_with_prompt
5
  import json
6
  import re
7
 
@@ -102,47 +102,17 @@ class PlannerManager:
102
  if not self.current_goal:
103
  return
104
 
105
- prompt = f"""
106
- # Instruction
107
- You are an AI assistant for an NPC named '{self.npc.korean_name}'.
108
- The NPC's goal is: "{self.current_goal.description}"
109
- Create a 3 to 5 step action plan to achieve this goal.
110
- Your response MUST be a valid JSON array of strings, where each string is a step in the plan.
111
- Do not include any other text, explanations, or markdown.
112
-
113
- # Example Output
114
- ["첫 번째 계획 단계.", "두 번째 계획 단계.", "세 번째 계획 단계."]
115
-
116
- # Plan (JSON array of strings only):
117
- """
118
- plan_text = query_llm_with_prompt(prompt).strip()
119
-
120
- if not plan_text or "[LLM Error]" in plan_text:
121
- return
122
-
123
- try:
124
- # JSON 응답을 직접 파싱하여 파이썬 리스트로 변환
125
- json_match = re.search(r'\[.*\]', plan_text, re.DOTALL)
126
- if not json_match:
127
- raise json.JSONDecodeError("No JSON array found", plan_text, 0)
128
-
129
- # 추출된 JSON 문자열 파싱
130
- cleaned_steps = json.loads(json_match.group())
131
- if not isinstance(cleaned_steps, list) or not all(isinstance(s, str) for s in cleaned_steps):
132
- raise json.JSONDecodeError("Invalid format", plan_text, 0)
133
- except (json.JSONDecodeError, TypeError):
134
- print(f"[{self.npc.korean_name}] 계획 생성 실패: LLM이 유효한 JSON을 반환하지 않았습니다.")
135
- print(f"LLM 응답: {plan_text}")
136
- self.current_plan = None
137
- return
138
 
139
  # 행동하기
140
- if cleaned_steps:
141
  with simulation_core.simulation_lock:
142
  self.current_plan = Plan(goal=self.current_goal, steps=cleaned_steps)
143
  print(f"[{self.npc.korean_name}의 계획 수립]")
144
  for i, step in enumerate(self.current_plan.steps):
145
  print(f" - {i+1}단계: {step}")
 
 
146
 
147
  def complete_step(self):
148
  """현재 단꼐를 완료하고 다음 단계로 진행. 계획이 모두 끝나면 모표를 완료 처리"""
 
1
  # portfolio/npc_social_network/npc/npc_planner.py
2
  from .. import simulation_core
3
  from typing import List, Optional, TYPE_CHECKING
4
+ from ..models.llm_helper import query_llm_with_prompt, generate_plan_from_goal
5
  import json
6
  import re
7
 
 
102
  if not self.current_goal:
103
  return
104
 
105
+ cleaned_steps = generate_plan_from_goal(self.npc.korean_name, self.current_goal.description)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  # 행동하기
108
+ if cleaned_steps and cleaned_steps[0] != "[계획 생성에 실패했습니다]":
109
  with simulation_core.simulation_lock:
110
  self.current_plan = Plan(goal=self.current_goal, steps=cleaned_steps)
111
  print(f"[{self.npc.korean_name}의 계획 수립]")
112
  for i, step in enumerate(self.current_plan.steps):
113
  print(f" - {i+1}단계: {step}")
114
+ else:
115
+ print(f"[{self.npc.korean_name}] 계획 생성에 최종 실패했습니다.")
116
 
117
  def complete_step(self):
118
  """현재 단꼐를 완료하고 다음 단계로 진행. 계획이 모두 끝나면 모표를 완료 처리"""
npc_social_network/simulation_core.py CHANGED
@@ -24,9 +24,11 @@ def add_log(message):
24
  """이벤트 로그에 메시지를 추가합니다."""
25
  timestamp = time.strftime("%H:%M:%S")
26
  print(f"[{timestamp}] {message}")
27
- event_log.insert(0, f"[{timestamp}] {message}")
28
- if len(event_log) > 100:
29
- event_log.pop()
 
 
30
 
31
  def tick_simulation():
32
  """시뮬레이션의 시간을 한 단계 진행"""
@@ -36,7 +38,7 @@ def tick_simulation():
36
  if simulation_paused: # 정지 상태에서는 틱 실행 x
37
  return
38
 
39
- add_log("시뮬레이션 틱 시작")
40
 
41
  # --- 자율 행동 및 상호작용은 이제 NPC가 스스로 결정 ---
42
  for npc in npc_manager.all():
 
24
  """이벤트 로그에 메시지를 추가합니다."""
25
  timestamp = time.strftime("%H:%M:%S")
26
  print(f"[{timestamp}] {message}")
27
+ event_log.append(f"[{timestamp}] {message}\n")
28
+
29
+ # 로그가 지정 수를 넘으면 가장 오래된 로그를 제거
30
+ if len(event_log) > 300:
31
+ event_log.pop(0)
32
 
33
  def tick_simulation():
34
  """시뮬레이션의 시간을 한 단계 진행"""
 
38
  if simulation_paused: # 정지 상태에서는 틱 실행 x
39
  return
40
 
41
+ print("시뮬레이션 틱 시작")
42
 
43
  # --- 자율 행동 및 상호작용은 이제 NPC가 스스로 결정 ---
44
  for npc in npc_manager.all():
npc_social_network/templates/dashboard.html CHANGED
@@ -24,7 +24,7 @@
24
  .info-panel { background: var(--panel-bg-color); padding: 20px; border-radius: 12px; box-shadow: var(--shadow); overflow-y: auto; }
25
  #npc-details { flex-basis: 45%; }
26
  #log-panel { flex-basis: 55%; display: flex; flex-direction: column; }
27
- #log-container { flex: 1; overflow-y: auto; font-size: 13px; line-height: 1.6; color: #495057; }
28
  h2 { margin-top: 0; margin-bottom: 15px; font-size: 18px; color: #343a40; }
29
  </style>
30
  </head>
@@ -149,7 +149,7 @@
149
 
150
  function updateLog(logMessages) {
151
  logContainer.textContent = logMessages.join('\n');
152
- logContainer.scrollTop = 0;
153
  }
154
 
155
  function updatePlayPauseButton(isPaused) {
 
24
  .info-panel { background: var(--panel-bg-color); padding: 20px; border-radius: 12px; box-shadow: var(--shadow); overflow-y: auto; }
25
  #npc-details { flex-basis: 45%; }
26
  #log-panel { flex-basis: 55%; display: flex; flex-direction: column; }
27
+ #log-container { flex: 1; overflow-y: auto; font-size: 13px; line-height: 1.6; color: #495057; white-space: pre-wrap; }
28
  h2 { margin-top: 0; margin-bottom: 15px; font-size: 18px; color: #343a40; }
29
  </style>
30
  </head>
 
149
 
150
  function updateLog(logMessages) {
151
  logContainer.textContent = logMessages.join('\n');
152
+ logContainer.scrollTop = logContainer.scrollHeight;
153
  }
154
 
155
  function updatePlayPauseButton(isPaused) {