Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
b03bfdc
1
Parent(s):
3d47d2b
배포 및 최종 수정 완료 버전
Browse files- app.py +5 -15
- 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/elin.faiss +0 -0
- npc_social_network/manager/conversation_manager.py +3 -4
- npc_social_network/models/gemini_setup.py +20 -5
- npc_social_network/models/llm_helper.py +58 -8
- npc_social_network/routes/npc_route.py +31 -4
- npc_social_network/simulation_core.py +11 -2
- templates/dashboard.html +81 -12
app.py
CHANGED
@@ -22,6 +22,8 @@ class LogFilter(logging.Filter):
|
|
22 |
# /api/world_state 경로에 대한 로그는 기록하지 않음
|
23 |
if "/api/world_state" in record.getMessage():
|
24 |
return False
|
|
|
|
|
25 |
return True
|
26 |
|
27 |
def create_app():
|
@@ -48,23 +50,11 @@ def create_app():
|
|
48 |
if __name__ == '__main__':
|
49 |
# 1. flask 앱 실행
|
50 |
app = create_app()
|
51 |
-
|
52 |
-
|
53 |
-
from npc_social_network import simulation_core
|
54 |
-
simulation_core.initialize_simulation()
|
55 |
-
print("✅ 시뮬레이션이 성공적으로 초기화되었습니다.")
|
56 |
-
except Exception as e:
|
57 |
-
import traceback
|
58 |
-
print("="*60)
|
59 |
-
print("❌ CRITICAL ERROR: 시뮬레이션 초기화 중 에러가 발생했습니다!")
|
60 |
-
print(f" 에러 메시지: {e}")
|
61 |
-
print("="*60)
|
62 |
-
traceback.print_exc()
|
63 |
-
print("="*60)
|
64 |
-
|
65 |
-
# 3. Flask 웹 서버 실행
|
66 |
log = logging.getLogger('werkzeug')
|
67 |
log.addFilter(LogFilter())
|
|
|
68 |
# use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
|
69 |
# 시뮬레이션 스레드가 두 번 시작되지 않도록 합니다.
|
70 |
app.run(debug=True, use_reloader=False, port=5000)
|
|
|
22 |
# /api/world_state 경로에 대한 로그는 기록하지 않음
|
23 |
if "/api/world_state" in record.getMessage():
|
24 |
return False
|
25 |
+
if "/api/npc_details" in record.getMessage():
|
26 |
+
return False
|
27 |
return True
|
28 |
|
29 |
def create_app():
|
|
|
50 |
if __name__ == '__main__':
|
51 |
# 1. flask 앱 실행
|
52 |
app = create_app()
|
53 |
+
|
54 |
+
# Flask 앱이 실행되기 전에 로그 필터를 적용합니다.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
log = logging.getLogger('werkzeug')
|
56 |
log.addFilter(LogFilter())
|
57 |
+
|
58 |
# use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
|
59 |
# 시뮬레이션 스레드가 두 번 시작되지 않도록 합니다.
|
60 |
app.run(debug=True, use_reloader=False, port=5000)
|
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/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/manager/conversation_manager.py
CHANGED
@@ -97,6 +97,9 @@ class ConversationManager:
|
|
97 |
|
98 |
simulation_core.add_log(f"[{initiator.korean_name}, {target.korean_name}] 대화 경험이 기억과 관계, 성격에 반영되었습니다.")
|
99 |
|
|
|
|
|
|
|
100 |
except Exception as e:
|
101 |
print(f"[에러] 백그라운드 기억 저장 중 문제 발생: {e}")
|
102 |
import traceback
|
@@ -137,10 +140,6 @@ class ConversationManager:
|
|
137 |
)
|
138 |
background_thread.start()
|
139 |
|
140 |
-
# 3. 대화가 종료된 직후, 현재까지의 시뮬레이션 전체 상태를 즉시 저장.
|
141 |
-
with simulation_core.simulation_lock:
|
142 |
-
simulation_core.save_simulation(simulation_core.npc_manager)
|
143 |
-
|
144 |
|
145 |
def next_turn(self):
|
146 |
"""대화의 다음 턴을 진행, 생성된 대사 처리"""
|
|
|
97 |
|
98 |
simulation_core.add_log(f"[{initiator.korean_name}, {target.korean_name}] 대화 경험이 기억과 관계, 성격에 반영되었습니다.")
|
99 |
|
100 |
+
# 9. 모든 백그라운드 작업 종료 후, 현재까지의 시뮬레이션 전체 상태를 즉시 저장.
|
101 |
+
simulation_core.save_simulation(simulation_core.npc_manager)
|
102 |
+
|
103 |
except Exception as e:
|
104 |
print(f"[에러] 백그라운드 기억 저장 중 문제 발생: {e}")
|
105 |
import traceback
|
|
|
140 |
)
|
141 |
background_thread.start()
|
142 |
|
|
|
|
|
|
|
|
|
143 |
|
144 |
def next_turn(self):
|
145 |
"""대화의 다음 턴을 진행, 생성된 대사 처리"""
|
npc_social_network/models/gemini_setup.py
CHANGED
@@ -4,10 +4,25 @@ from dotenv import load_dotenv
|
|
4 |
import os
|
5 |
|
6 |
|
7 |
-
def load_gemini():
|
8 |
-
|
9 |
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
import os
|
5 |
|
6 |
|
7 |
+
# def load_gemini():
|
8 |
+
# load_dotenv() # .env 파일에서 환경 변수 로드
|
9 |
|
10 |
+
# genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
11 |
+
# model = genai.GenerativeModel("gemini-2.0-flash") # Gemini Flash 사용
|
12 |
|
13 |
+
# return model
|
14 |
+
|
15 |
+
def initialize_model(model_name: str, api_key: str):
|
16 |
+
""" 주어진 모델과 API 키로 Gemini 모델을 동적으로 초기화"""
|
17 |
+
try:
|
18 |
+
# API 키를 설정
|
19 |
+
genai.configure(api_key=api_key)
|
20 |
+
# 지정된 모델을 생성
|
21 |
+
model = genai.GenerativeModel(model_name)
|
22 |
+
# 간단한 테스트 호출로 API 키 유효성 검증
|
23 |
+
model.generate_content("hello", generation_config={"max_output_tokens": 1})
|
24 |
+
print(f"✅ Gemini 모델 '{model_name}'이(가) 성공적으로 초기화되었습니다.")
|
25 |
+
return model
|
26 |
+
except Exception as e:
|
27 |
+
print(f"❌ Gemini 모델 초기화 실패: {e}")
|
28 |
+
return None
|
npc_social_network/models/llm_helper.py
CHANGED
@@ -1,20 +1,19 @@
|
|
1 |
# npc_social_network/models/llm_helper.py
|
2 |
|
3 |
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 |
import json
|
8 |
import re
|
|
|
|
|
9 |
from typing import TYPE_CHECKING, List
|
10 |
|
11 |
if TYPE_CHECKING:
|
12 |
from ..npc.npc_base import NPC
|
13 |
from ..manager.conversation_manager import Conversation
|
14 |
|
15 |
-
# Gemini 모델 초기화
|
16 |
-
gemini_model = load_gemini()
|
17 |
-
|
18 |
def query_llm_for_emotion(user_input):
|
19 |
"""
|
20 |
LLM을 통해 플레이어 입력에서 감정 추출
|
@@ -28,7 +27,7 @@ def query_llm_for_emotion(user_input):
|
|
28 |
반드시 감정 이름을 한 개만 출력하세요. (예: joy)
|
29 |
"""
|
30 |
try:
|
31 |
-
response =
|
32 |
return response.text.strip()
|
33 |
except Exception as e:
|
34 |
return f"[LLM Error] {str(e)}"
|
@@ -36,11 +35,57 @@ def query_llm_for_emotion(user_input):
|
|
36 |
def query_llm_with_prompt(prompt: str) -> str:
|
37 |
"""
|
38 |
prompt 문자열을 받아 LLM 호출
|
|
|
39 |
"""
|
|
|
|
|
|
|
40 |
try:
|
41 |
-
|
|
|
42 |
return response.text.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
except Exception as e:
|
|
|
44 |
return f"[LLM Error] {str(e)}"
|
45 |
|
46 |
def summarize_text(text: str) -> str:
|
@@ -163,6 +208,8 @@ def _query_llm_for_json_robustly(prompt: str) -> dict | list:
|
|
163 |
else:
|
164 |
text_to_parse = response_text # 최후의 경우 원본 텍스트 사용
|
165 |
|
|
|
|
|
166 |
# 딕셔너리나 리스트의 마지막 요소 뒤에 붙은 꼬리표 쉼표 제거
|
167 |
text_to_parse = re.sub(r',\s*(\}|\])', r'\1', text_to_parse)
|
168 |
|
@@ -183,6 +230,9 @@ Do not include any other text, explanations, or markdown.
|
|
183 |
# Corrected JSON:
|
184 |
"""
|
185 |
corrected_response = query_llm_with_prompt(correction_prompt)
|
|
|
|
|
|
|
186 |
try:
|
187 |
return json.loads(corrected_response) # 교정된 응답 파싱 시도
|
188 |
except json.JSONDecodeError:
|
@@ -368,11 +418,11 @@ def evaluate_goal_achievement(initiator_name: str, goal: str, conversation_trans
|
|
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}
|
|
|
1 |
# npc_social_network/models/llm_helper.py
|
2 |
|
3 |
from npc_social_network.npc.emotion_config import EMOTION_LIST
|
|
|
4 |
from npc_social_network.npc.npc_memory import Memory
|
5 |
+
from .. import simulation_core
|
6 |
from typing import List
|
7 |
import json
|
8 |
import re
|
9 |
+
import time
|
10 |
+
from google.api_core import exceptions as google_exceptions
|
11 |
from typing import TYPE_CHECKING, List
|
12 |
|
13 |
if TYPE_CHECKING:
|
14 |
from ..npc.npc_base import NPC
|
15 |
from ..manager.conversation_manager import Conversation
|
16 |
|
|
|
|
|
|
|
17 |
def query_llm_for_emotion(user_input):
|
18 |
"""
|
19 |
LLM을 통해 플레이어 입력에서 감정 추출
|
|
|
27 |
반드시 감정 이름을 한 개만 출력하세요. (예: joy)
|
28 |
"""
|
29 |
try:
|
30 |
+
response = simulation_core.active_llm_model.generate_content(prompt)
|
31 |
return response.text.strip()
|
32 |
except Exception as e:
|
33 |
return f"[LLM Error] {str(e)}"
|
|
|
35 |
def query_llm_with_prompt(prompt: str) -> str:
|
36 |
"""
|
37 |
prompt 문자열을 받아 LLM 호출
|
38 |
+
- api 사용량 오류를 감지하고 자동 대처
|
39 |
"""
|
40 |
+
if not simulation_core.active_llm_model:
|
41 |
+
return "[LLM Error] API 모델이 정상적으로 설정되지 않았습니다."
|
42 |
+
|
43 |
try:
|
44 |
+
# 1. 첫 번째 API 호출 시도
|
45 |
+
response = simulation_core.active_llm_model.generate_content(prompt)
|
46 |
return response.text.strip()
|
47 |
+
except google_exceptions.ResourceExhausted as e:
|
48 |
+
error_message = str(e)
|
49 |
+
|
50 |
+
# 2. 오류 메시지를 분석하여 원인 파악
|
51 |
+
if "PerMinute" in error_message:
|
52 |
+
# -- 분당 사용량 초과 --
|
53 |
+
simulation_core.add_log("[API 경고] 분당 사용량을 초과했습니다. 60초 후 자동으로 재시도 합니다.")
|
54 |
+
time.sleep(60)
|
55 |
+
|
56 |
+
try:
|
57 |
+
# 2-1. 두 번째 API 호출 시도
|
58 |
+
simulation_core.add_log("[API 정보] API 호출을 재시도합니다.")
|
59 |
+
response = simulation_core.active_llm_model.generate_content(prompt)
|
60 |
+
return response.text.strip()
|
61 |
+
except Exception as retry_e:
|
62 |
+
# 재시도마저 실패할 경우, 최종 에러 반환
|
63 |
+
simulation_core.add_log("========= [API Error] =========")
|
64 |
+
|
65 |
+
# 시물레이션을 안전하게 '일시정지' 상태로 변경
|
66 |
+
with simulation_core.simulation_lock:
|
67 |
+
simulation_core.simulation_paused = True
|
68 |
+
|
69 |
+
return f"[API 에러] 재시도에 실패했습니다: {retry_e}"
|
70 |
+
|
71 |
+
elif "PerDay" in error_message:
|
72 |
+
# -- 일일 사용량 초과 시 --
|
73 |
+
simulation_core.add_log("[API Error] 일일 사용량을 모두 소진했습니다.")
|
74 |
+
simulation_core.add_log("다른 API 키를 사용하거나, 내일 다시 시도해주세요.")
|
75 |
+
|
76 |
+
# 시뮬레이션을 안전하게 '일시정지' 상태로 변경
|
77 |
+
with simulation_core.simulation_lock:
|
78 |
+
simulation_core.simulation_paused = True
|
79 |
+
return "[LLM Error] Daily quota exceeded"
|
80 |
+
|
81 |
+
else:
|
82 |
+
# 그 외 다른 ResourceExhausted 오류
|
83 |
+
with simulation_core.simulation_lock:
|
84 |
+
simulation_core.simulation_paused = True
|
85 |
+
return f"[LLM Error] {error_message}"
|
86 |
+
|
87 |
except Exception as e:
|
88 |
+
# 그 외 모든 종류의 오류
|
89 |
return f"[LLM Error] {str(e)}"
|
90 |
|
91 |
def summarize_text(text: str) -> str:
|
|
|
208 |
else:
|
209 |
text_to_parse = response_text # 최후의 경우 원본 텍스트 사용
|
210 |
|
211 |
+
# 불리언 값의 대소문자를 표준에 맞게 수정 (True -> true)
|
212 |
+
text_to_parse = text_to_parse.replace(": True", ": true").replace(": False", ": false")
|
213 |
# 딕셔너리나 리스트의 마지막 요소 뒤에 붙은 꼬리표 쉼표 제거
|
214 |
text_to_parse = re.sub(r',\s*(\}|\])', r'\1', text_to_parse)
|
215 |
|
|
|
230 |
# Corrected JSON:
|
231 |
"""
|
232 |
corrected_response = query_llm_with_prompt(correction_prompt)
|
233 |
+
# 자가 교정된 텍스트도 한번 더 정리
|
234 |
+
corrected_response = corrected_response.replace(": True", ": true").replace(": False", ": false")
|
235 |
+
corrected_response = re.sub(r',\s*(\}|\])', r'\1', corrected_response)
|
236 |
try:
|
237 |
return json.loads(corrected_response) # 교정된 응답 파싱 시도
|
238 |
except json.JSONDecodeError:
|
|
|
418 |
결과는 반드시 아래 JSON 형식이어야 합니다.
|
419 |
|
420 |
# Evaluation (JSON format only)
|
421 |
+
{{
|
422 |
"goal_achieved": True,
|
423 |
"reason": "목표 달성 또는 실패에 대한 간결한 이유 (예: '상대방을 위로하고 지지하며 긍정적인 반응을 이끌어냈다.)",
|
424 |
"initiator_emotion": "이 결과를 얻은 후 대화 시작자가 느낄 감정 (아래 'Emotion_List에서 찾아서 한 단어로 표현)
|
425 |
+
}}
|
426 |
|
427 |
# Emotion_List
|
428 |
{EMOTION_LIST}
|
npc_social_network/routes/npc_route.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
# portfolio/npc_social_network/routes/npc_route.py
|
2 |
from flask import Blueprint, render_template, request, jsonify, url_for
|
3 |
from .. import simulation_core
|
|
|
4 |
import os
|
5 |
|
6 |
|
@@ -8,15 +9,43 @@ import os
|
|
8 |
npc_bp = Blueprint(
|
9 |
"npc_social",
|
10 |
__name__,
|
11 |
-
static_folder="../static"
|
|
|
12 |
)
|
13 |
|
14 |
@npc_bp.route("/")
|
15 |
def dashboard():
|
16 |
return render_template("dashboard.html")
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
@npc_bp.route("/api/world_state", methods=['GET'])
|
19 |
def get_world_state():
|
|
|
|
|
20 |
with simulation_core.simulation_lock:
|
21 |
if not simulation_core.npc_manager:
|
22 |
return jsonify({"error": "Simulation not started"}), 500
|
@@ -26,7 +55,7 @@ def get_world_state():
|
|
26 |
|
27 |
nodes = []
|
28 |
for npc in all_npcs:
|
29 |
-
# 각 NPC의 영어 ID를 기반으로 이미지 파일명을 생성합니다. (예: elin ->
|
30 |
image_filename = f"images/npc/{npc.name}.png"
|
31 |
# if not os.path.exists(image_filename):
|
32 |
# image_filename = f"images/npc/npc.png"
|
@@ -78,8 +107,6 @@ def get_world_state():
|
|
78 |
|
79 |
return jsonify({ "nodes": nodes, "edges": edges, "log": current_log, "paused": is_paused, "waiting_for_player": waiting_for_player, "player_conversation": player_conversation_info })
|
80 |
|
81 |
-
# 다른 API 엔드포인트들은 이 테스트 동안에는 호출되지 않으므로 그대로 두어도 괜찮습니다.
|
82 |
-
|
83 |
@npc_bp.route("/api/toggle_simulation", methods=['POST'])
|
84 |
def toggle_simulation():
|
85 |
with simulation_core.simulation_lock:
|
|
|
1 |
# portfolio/npc_social_network/routes/npc_route.py
|
2 |
from flask import Blueprint, render_template, request, jsonify, url_for
|
3 |
from .. import simulation_core
|
4 |
+
from ..models.gemini_setup import initialize_model
|
5 |
import os
|
6 |
|
7 |
|
|
|
9 |
npc_bp = Blueprint(
|
10 |
"npc_social",
|
11 |
__name__,
|
12 |
+
static_folder="../static",
|
13 |
+
static_url_path="/npc_social_network"
|
14 |
)
|
15 |
|
16 |
@npc_bp.route("/")
|
17 |
def dashboard():
|
18 |
return render_template("dashboard.html")
|
19 |
|
20 |
+
# 시뮬레이션을 시작하는 API
|
21 |
+
@npc_bp.route("/api/initialize_simulation", methods=['POST'])
|
22 |
+
def api_initialize_simulation():
|
23 |
+
"""API 키를 받아 시뮬레이션을 초기화하고 시작하는 API"""
|
24 |
+
from ..models.gemini_setup import initialize_model
|
25 |
+
data = request.json
|
26 |
+
model_name = data.get("model_name", "gemini-2.0-flash")
|
27 |
+
api_key = data.get("api_key")
|
28 |
+
|
29 |
+
if not api_key:
|
30 |
+
return jsonify({"success": False, "error": "API 키를 입력해주세요."}), 400
|
31 |
+
|
32 |
+
# 1. 모델 초기화 시도
|
33 |
+
new_model = initialize_model(model_name, api_key)
|
34 |
+
|
35 |
+
if new_model:
|
36 |
+
# 2. 성공시, 전역 모델을 설정하고 시뮬레이션을 시작
|
37 |
+
simulation_core.active_llm_model = new_model
|
38 |
+
if not simulation_core.simulation_initialized:
|
39 |
+
simulation_core.initialize_simulation()
|
40 |
+
simulation_core.start_simulation_loop()
|
41 |
+
return jsonify({"success": True, "message": f"'{model_name}' 모델로 시뮬레이션을 시작합니다."})
|
42 |
+
else:
|
43 |
+
return jsonify({"success": False, "error": "API 키가 유효하지 않거나 모델 초기화에 실패했습니다."}), 400
|
44 |
+
|
45 |
@npc_bp.route("/api/world_state", methods=['GET'])
|
46 |
def get_world_state():
|
47 |
+
if not simulation_core.simulation_initialized:
|
48 |
+
return jsonify({"status": "needs_initialization", "log": simulation_core.event_log})
|
49 |
with simulation_core.simulation_lock:
|
50 |
if not simulation_core.npc_manager:
|
51 |
return jsonify({"error": "Simulation not started"}), 500
|
|
|
55 |
|
56 |
nodes = []
|
57 |
for npc in all_npcs:
|
58 |
+
# 각 NPC의 영어 ID를 기반으로 이미지 파일명을 생성합니다. (예: elin -> elin.png)
|
59 |
image_filename = f"images/npc/{npc.name}.png"
|
60 |
# if not os.path.exists(image_filename):
|
61 |
# image_filename = f"images/npc/npc.png"
|
|
|
107 |
|
108 |
return jsonify({ "nodes": nodes, "edges": edges, "log": current_log, "paused": is_paused, "waiting_for_player": waiting_for_player, "player_conversation": player_conversation_info })
|
109 |
|
|
|
|
|
110 |
@npc_bp.route("/api/toggle_simulation", methods=['POST'])
|
111 |
def toggle_simulation():
|
112 |
with simulation_core.simulation_lock:
|
npc_social_network/simulation_core.py
CHANGED
@@ -15,6 +15,8 @@ from typing import List, Optional
|
|
15 |
# 1. 시뮬레이션 상태 (전역 변수)
|
16 |
# - 이 변수들이 시뮬레이션 세계의 모든 정보를 담습니다.
|
17 |
#------------------------------------------
|
|
|
|
|
18 |
npc_manager: NPCManager = None
|
19 |
simulation_paused = True
|
20 |
event_log:List[str] = []
|
@@ -120,16 +122,23 @@ def simulation_loop():
|
|
120 |
time.sleep(5) # 5초에 한 번씩 틱 발생 (이것보다 줄이면 Gemini API 사용량 초과 발생 가능)
|
121 |
|
122 |
def initialize_simulation():
|
123 |
-
"""
|
124 |
-
global npc_manager, conversation_manager
|
|
|
|
|
|
|
|
|
|
|
125 |
npc_manager = load_simulation()
|
126 |
conversation_manager = ConversationManager()
|
127 |
if npc_manager is None:
|
128 |
npc_manager = setup_initial_scenario()
|
129 |
save_simulation(npc_manager)
|
130 |
|
|
|
131 |
add_log("시뮬레이션이 성공적으로 초기화되었습니다.")
|
132 |
|
|
|
133 |
# 백그라운드 스레드에서 시뮬레이션 루프 시작
|
134 |
simulation_thread = threading.Thread(target=simulation_loop, daemon=True)
|
135 |
simulation_thread.start()
|
|
|
15 |
# 1. 시뮬레이션 상태 (전역 변수)
|
16 |
# - 이 변수들이 시뮬레이션 세계의 모든 정보를 담습니다.
|
17 |
#------------------------------------------
|
18 |
+
simulation_initialized = False # 초기화 여부 플래그
|
19 |
+
active_llm_model = None # 현재 활성화된 LLM 모델
|
20 |
npc_manager: NPCManager = None
|
21 |
simulation_paused = True
|
22 |
event_log:List[str] = []
|
|
|
122 |
time.sleep(5) # 5초에 한 번씩 틱 발생 (이것보다 줄이면 Gemini API 사용량 초과 발생 가능)
|
123 |
|
124 |
def initialize_simulation():
|
125 |
+
"""시뮬레이션의 준비"""
|
126 |
+
global npc_manager, conversation_manager, simulation_initialized, active_llm_model
|
127 |
+
|
128 |
+
# 이미 초기화되었다면 다시 실행 x
|
129 |
+
if simulation_initialized:
|
130 |
+
return
|
131 |
+
|
132 |
npc_manager = load_simulation()
|
133 |
conversation_manager = ConversationManager()
|
134 |
if npc_manager is None:
|
135 |
npc_manager = setup_initial_scenario()
|
136 |
save_simulation(npc_manager)
|
137 |
|
138 |
+
simulation_initialized = True
|
139 |
add_log("시뮬레이션이 성공적으로 초기화되었습니다.")
|
140 |
|
141 |
+
def start_simulation_loop():
|
142 |
# 백그라운드 스레드에서 시뮬레이션 루프 시작
|
143 |
simulation_thread = threading.Thread(target=simulation_loop, daemon=True)
|
144 |
simulation_thread.start()
|
templates/dashboard.html
CHANGED
@@ -20,6 +20,14 @@
|
|
20 |
#player-input-text { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; }
|
21 |
#player-send-btn { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: white; background-color: var(--primary-color); }
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
/* 전체 컨트롤 패널 (가장 바깥쪽) */
|
24 |
#controls { flex-shrink: 0; padding: 12px; display: flex; flex-direction: column; gap: 15px; background-color: #3e3f41; border-radius: 0 0 12px 12px;}
|
25 |
|
@@ -54,6 +62,20 @@
|
|
54 |
</style>
|
55 |
</head>
|
56 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
<div id="main-container">
|
58 |
<div id="network-panel">
|
59 |
<div id="network"></div>
|
@@ -69,7 +91,7 @@
|
|
69 |
<div id="main-controls" style="display: flex; gap: 10px; justify-content: flex-start;">
|
70 |
<button id="play-pause-btn">Play / Pause</button>
|
71 |
<button id="tick-btn">Next Tick</button>
|
72 |
-
<button id="player-toggle-btn">플레이어 비활성화</button>
|
73 |
<button id="fit-btn">관계도 보기</button>
|
74 |
</div>
|
75 |
<!-- 개입 기능 선택 -->
|
@@ -124,6 +146,13 @@
|
|
124 |
|
125 |
<script>
|
126 |
document.addEventListener('DOMContentLoaded', async function () {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
// UI 요소 가져오기
|
128 |
const networkContainer = document.getElementById('network');
|
129 |
const logContainer = document.getElementById('log-container');
|
@@ -172,12 +201,46 @@
|
|
172 |
interaction: { hover: true, zoomView: true, dragView: true, minZoom: 0.2, maxZoom: 4.0 }
|
173 |
};
|
174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
fitBtn.addEventListener('click', () => {
|
176 |
if (network) {
|
177 |
network.fit();
|
178 |
}
|
179 |
});
|
180 |
-
|
181 |
// 탭 버튼 클릭 이벤트
|
182 |
tabEvent.addEventListener('click', () =>{
|
183 |
tabEvent.classList.add('active');
|
@@ -213,7 +276,7 @@
|
|
213 |
if (!npc1_name || !npc2_name || !situation || npc1_name === npc2_name) {
|
214 |
return alert ('서로 다른 두 NPC와 상황을 모두 입력해주세요.');
|
215 |
}
|
216 |
-
await fetch('/
|
217 |
method: 'POST',
|
218 |
headers: { 'Content-Type': 'application/json' },
|
219 |
body: JSON.stringify({ npc1_name, npc2_name, situation })
|
@@ -223,8 +286,14 @@
|
|
223 |
|
224 |
async function updateWorld() {
|
225 |
try {
|
226 |
-
const response = await fetch('/
|
227 |
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
if (data.error) throw new Error(data.error);
|
229 |
|
230 |
// 데이터셋을 직접 업데이트하면 vis-network가 알아서 변경사항을 감지하고 다시 그립니다.
|
@@ -280,7 +349,7 @@
|
|
280 |
const npcName = selectedNodeObject.label; // 영어 ID가 아닌 한글 라벨로 요청
|
281 |
|
282 |
try {
|
283 |
-
const response = await fetch(`/
|
284 |
const details = await response.json();
|
285 |
let detailsHtml = `<h3>${details.name} (${details.job}, ${details.age}세)</h3>`;
|
286 |
detailsHtml += `<p><strong>성격:</strong> ${details.personality_summary}</p>`;
|
@@ -333,7 +402,7 @@
|
|
333 |
if (!npc1_name || !npc2_name || npc1_name === npc2_name) {
|
334 |
return alert('서로 다른 두 명의 NPC를 선택해주세요.');
|
335 |
}
|
336 |
-
await fetch('/
|
337 |
method: 'POST',
|
338 |
headers: { 'Content-Type': 'application/json' },
|
339 |
body: JSON.stringify({ npc1_name, npc2_name, relationship_type })
|
@@ -343,16 +412,16 @@
|
|
343 |
|
344 |
// 모든 이벤트 리스너 복구
|
345 |
playPauseBtn.addEventListener('click', async () => {
|
346 |
-
const response = await fetch('/
|
347 |
const data = await response.json();
|
348 |
updatePlayPauseButton(data.paused);
|
349 |
});
|
350 |
-
tickBtn.addEventListener('click', async () => { await fetch('/
|
351 |
injectEventBtn.addEventListener('click', async () => {
|
352 |
const npc_name = eventNpcSelect.value; // 이제 한글 이름이 전송됩니다.
|
353 |
const event_text = eventTextInput.value;
|
354 |
if (!npc_name || !event_text) return alert('NPC와 이벤트 내용을 모두 입력해주세요.');
|
355 |
-
await fetch('/
|
356 |
method: 'POST',
|
357 |
headers: { 'Content-Type': 'application/json' },
|
358 |
body: JSON.stringify({ npc_name, event_text })
|
@@ -361,10 +430,10 @@
|
|
361 |
await updateWorld();
|
362 |
});
|
363 |
playerToggleBtn.addEventListener('click', async () => {
|
364 |
-
const response = await fetch('/
|
365 |
const data = await response.json();
|
366 |
// 버튼 텍스트 업데이트
|
367 |
-
playerToggleBtn.textContent = data.player_is_active ? '플레이어
|
368 |
});
|
369 |
// '전송' 버튼 이벤트 리스너
|
370 |
playerSendBtn.addEventListener('click', async () => {
|
@@ -372,7 +441,7 @@
|
|
372 |
if (!utterance) return;
|
373 |
|
374 |
playerSendBtn.disabled = true;
|
375 |
-
await fetch('/
|
376 |
method: 'POST',
|
377 |
headers: { 'Content-Type': 'application/json' },
|
378 |
body: JSON.stringify({ utterance })
|
|
|
20 |
#player-input-text { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; }
|
21 |
#player-send-btn { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: white; background-color: var(--primary-color); }
|
22 |
|
23 |
+
/* API 설정 */
|
24 |
+
#api-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 1000; }
|
25 |
+
#api-modal-content { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 400px; text-align: center; }
|
26 |
+
#api-modal-content h2 { margin-top: 0; }
|
27 |
+
#api-modal-content select, #api-modal-content input, #api-modal-content button { width: 100%; padding: 12px; margin-top: 10px; box-sizing: border-box; border-radius: 6px; border: 1px solid #ccc; }
|
28 |
+
#api-modal-content button { background: var(--primary-color); color: white; cursor: pointer; font-weight: bold; }
|
29 |
+
.hidden { display: none !important; }
|
30 |
+
|
31 |
/* 전체 컨트롤 패널 (가장 바깥쪽) */
|
32 |
#controls { flex-shrink: 0; padding: 12px; display: flex; flex-direction: column; gap: 15px; background-color: #3e3f41; border-radius: 0 0 12px 12px;}
|
33 |
|
|
|
62 |
</style>
|
63 |
</head>
|
64 |
<body>
|
65 |
+
<div id="api-modal-overlay">
|
66 |
+
<div id="api-modal-content">
|
67 |
+
<h2>시뮬레이션 시작</h2>
|
68 |
+
<p>사용할 LLM 모델과 API 키를 입력해주세요.</p>
|
69 |
+
<select id="llm-model-select">
|
70 |
+
<option value="gemini-2.0-flash">[추천] Gemini 2.0 Flash</option>
|
71 |
+
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
|
72 |
+
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
|
73 |
+
</select>
|
74 |
+
<input type="password" id="api-key-input" placeholder="API Key를 여기에 붙여넣으세요">
|
75 |
+
<button id="start-simulation-btn">시작하기</button>
|
76 |
+
<p id="api-modal-message" style="color: red; margin-top: 10px;"></p>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
<div id="main-container">
|
80 |
<div id="network-panel">
|
81 |
<div id="network"></div>
|
|
|
91 |
<div id="main-controls" style="display: flex; gap: 10px; justify-content: flex-start;">
|
92 |
<button id="play-pause-btn">Play / Pause</button>
|
93 |
<button id="tick-btn">Next Tick</button>
|
94 |
+
<button id="player-toggle-btn">플레이어 상태: 비활성화</button>
|
95 |
<button id="fit-btn">관계도 보기</button>
|
96 |
</div>
|
97 |
<!-- 개입 기능 선택 -->
|
|
|
146 |
|
147 |
<script>
|
148 |
document.addEventListener('DOMContentLoaded', async function () {
|
149 |
+
// API 입력
|
150 |
+
const apiModalOverlay = document.getElementById('api-modal-overlay');
|
151 |
+
const llmModelSelect = document.getElementById('llm-model-select');
|
152 |
+
const apiKeyInput = document.getElementById('api-key-input');
|
153 |
+
const startSimBtn = document.getElementById('start-simulation-btn');
|
154 |
+
const apiModalMsg = document.getElementById('api-modal-message');
|
155 |
+
|
156 |
// UI 요소 가져오기
|
157 |
const networkContainer = document.getElementById('network');
|
158 |
const logContainer = document.getElementById('log-container');
|
|
|
201 |
interaction: { hover: true, zoomView: true, dragView: true, minZoom: 0.2, maxZoom: 4.0 }
|
202 |
};
|
203 |
|
204 |
+
// 시작 버튼 이벤트
|
205 |
+
startSimBtn.addEventListener('click', async () => {
|
206 |
+
const model_name = llmModelSelect.value;
|
207 |
+
const api_key = apiKeyInput.value;
|
208 |
+
|
209 |
+
if (!api_key) {
|
210 |
+
apiModalMsg.textContent = 'API 키를 입력해주세요.';
|
211 |
+
return;
|
212 |
+
}
|
213 |
+
|
214 |
+
startSimBtn.textContent = '인증 중...';
|
215 |
+
startSimBtn.disabled = true;
|
216 |
+
apiModalMsg.textContent = '';
|
217 |
+
|
218 |
+
const response = await fetch('/api/initialize_simulation', {
|
219 |
+
method: 'POST',
|
220 |
+
headers: { 'Content-Type': 'application/json' },
|
221 |
+
body: JSON.stringify({ model_name, api_key })
|
222 |
+
});
|
223 |
+
const data = await response.json();
|
224 |
+
|
225 |
+
if (data.success) {
|
226 |
+
apiModalOverlay.classList.add('hidden'); // 모달 숨기기
|
227 |
+
// 시뮬레이션이 성공적으로 시작되었으므로, 이제부터 월드 상태를 주기적으로 업데이트합니다.
|
228 |
+
await updateWorld();
|
229 |
+
if (!autoUpdateInterval) { // 중복 실행 방지
|
230 |
+
autoUpdateInterval = setInterval(updateWorld, 5000);
|
231 |
+
}
|
232 |
+
} else {
|
233 |
+
apiModalMsg.textContent = `오류: ${data.error}`;
|
234 |
+
startSimBtn.textContent = '시작하기';
|
235 |
+
startSimBtn.disabled = false;
|
236 |
+
}
|
237 |
+
});
|
238 |
+
|
239 |
fitBtn.addEventListener('click', () => {
|
240 |
if (network) {
|
241 |
network.fit();
|
242 |
}
|
243 |
});
|
|
|
244 |
// 탭 버튼 클릭 이벤트
|
245 |
tabEvent.addEventListener('click', () =>{
|
246 |
tabEvent.classList.add('active');
|
|
|
276 |
if (!npc1_name || !npc2_name || !situation || npc1_name === npc2_name) {
|
277 |
return alert ('서로 다른 두 NPC와 상황을 모두 입력해주세요.');
|
278 |
}
|
279 |
+
await fetch('/api/orchestrate_conversation', {
|
280 |
method: 'POST',
|
281 |
headers: { 'Content-Type': 'application/json' },
|
282 |
body: JSON.stringify({ npc1_name, npc2_name, situation })
|
|
|
286 |
|
287 |
async function updateWorld() {
|
288 |
try {
|
289 |
+
const response = await fetch('/api/world_state');
|
290 |
const data = await response.json();
|
291 |
+
// 서버가 아직 준비되지 않았음을 확인하고 대기
|
292 |
+
if (data.status === 'needs_initialization') {
|
293 |
+
console.log("시뮬레이션 초기화 대기 중...");
|
294 |
+
updateLog(data.log); // 초기 로그는 표시
|
295 |
+
return;
|
296 |
+
}
|
297 |
if (data.error) throw new Error(data.error);
|
298 |
|
299 |
// 데이터셋을 직접 업데이트하면 vis-network가 알아서 변경사항을 감지하고 다시 그립니다.
|
|
|
349 |
const npcName = selectedNodeObject.label; // 영어 ID가 아닌 한글 라벨로 요청
|
350 |
|
351 |
try {
|
352 |
+
const response = await fetch(`/api/npc_details/${npcName}`);
|
353 |
const details = await response.json();
|
354 |
let detailsHtml = `<h3>${details.name} (${details.job}, ${details.age}세)</h3>`;
|
355 |
detailsHtml += `<p><strong>성격:</strong> ${details.personality_summary}</p>`;
|
|
|
402 |
if (!npc1_name || !npc2_name || npc1_name === npc2_name) {
|
403 |
return alert('서로 다른 두 명의 NPC를 선택해주세요.');
|
404 |
}
|
405 |
+
await fetch('/api/force_relationship', {
|
406 |
method: 'POST',
|
407 |
headers: { 'Content-Type': 'application/json' },
|
408 |
body: JSON.stringify({ npc1_name, npc2_name, relationship_type })
|
|
|
412 |
|
413 |
// 모든 이벤트 리스너 복구
|
414 |
playPauseBtn.addEventListener('click', async () => {
|
415 |
+
const response = await fetch('/api/toggle_simulation', { method: 'POST' });
|
416 |
const data = await response.json();
|
417 |
updatePlayPauseButton(data.paused);
|
418 |
});
|
419 |
+
tickBtn.addEventListener('click', async () => { await fetch('/api/manual_tick', { method: 'POST' }); await updateWorld(); });
|
420 |
injectEventBtn.addEventListener('click', async () => {
|
421 |
const npc_name = eventNpcSelect.value; // 이제 한글 이름이 전송됩니다.
|
422 |
const event_text = eventTextInput.value;
|
423 |
if (!npc_name || !event_text) return alert('NPC와 이벤트 내용을 모두 입력해주세요.');
|
424 |
+
await fetch('/api/inject_event', {
|
425 |
method: 'POST',
|
426 |
headers: { 'Content-Type': 'application/json' },
|
427 |
body: JSON.stringify({ npc_name, event_text })
|
|
|
430 |
await updateWorld();
|
431 |
});
|
432 |
playerToggleBtn.addEventListener('click', async () => {
|
433 |
+
const response = await fetch('/api/toggle_player', {method: 'POST'});
|
434 |
const data = await response.json();
|
435 |
// 버튼 텍스트 업데이트
|
436 |
+
playerToggleBtn.textContent = data.player_is_active ? '플레이어 상태: 활성화' : '플레이어 상태: 비활성화';
|
437 |
});
|
438 |
// '전송' 버튼 이벤트 리스너
|
439 |
playerSendBtn.addEventListener('click', async () => {
|
|
|
441 |
if (!utterance) return;
|
442 |
|
443 |
playerSendBtn.disabled = true;
|
444 |
+
await fetch('/api/player_response', {
|
445 |
method: 'POST',
|
446 |
headers: { 'Content-Type': 'application/json' },
|
447 |
body: JSON.stringify({ utterance })
|