Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
2cd8c90
1
Parent(s):
033af6f
[테스트중]사용자 개입 구현
Browse files
npc_social_network/npc/npc_relationship.py
CHANGED
@@ -2,6 +2,8 @@
|
|
2 |
# portfolio/npc_social_network/npc/npc_relationship.py
|
3 |
|
4 |
from typing import List, Optional, TYPE_CHECKING
|
|
|
|
|
5 |
if TYPE_CHECKING:
|
6 |
from .npc_memory import Memory
|
7 |
from .npc_base import NPC
|
@@ -82,6 +84,36 @@ class RelationshipManager:
|
|
82 |
profile = self._get_or_create_profile(target_name)
|
83 |
return profile.summary
|
84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
def summarize_relationship(self, target_name: str, npc_manager: "NPCManager"):
|
86 |
""" LLM을 사용하여 특정 대상과의 관계를 주기적으로 요약하고 업데이트"""
|
87 |
from ..models.llm_helper import query_llm_with_prompt
|
|
|
2 |
# portfolio/npc_social_network/npc/npc_relationship.py
|
3 |
|
4 |
from typing import List, Optional, TYPE_CHECKING
|
5 |
+
from .npc_manager import get_korean_postposition
|
6 |
+
|
7 |
if TYPE_CHECKING:
|
8 |
from .npc_memory import Memory
|
9 |
from .npc_base import NPC
|
|
|
84 |
profile = self._get_or_create_profile(target_name)
|
85 |
return profile.summary
|
86 |
|
87 |
+
def set_relationship(self, target_name: str, relationship_type: str):
|
88 |
+
"""플레이어 개입 등으로 관계 유형과 점수를 직접 설정"""
|
89 |
+
from .. import simulation_core
|
90 |
+
|
91 |
+
profile = self._get_or_create_profile(target_name)
|
92 |
+
|
93 |
+
# 설정된 타입에 따라 점수를 부여 (값 조절 가능)
|
94 |
+
score_map = {
|
95 |
+
"best friend": 80.0,
|
96 |
+
"friend": 50.0,
|
97 |
+
"acquaintance": 10.0,
|
98 |
+
"stranger": 0.0,
|
99 |
+
"nuisance": -10.0,
|
100 |
+
"rival": -50.0,
|
101 |
+
"enemy": -80.0
|
102 |
+
}
|
103 |
+
|
104 |
+
profile.type = relationship_type
|
105 |
+
profile.score = score_map.get(relationship_type, 0.0)
|
106 |
+
|
107 |
+
target_npc = self.owner_npc.manager.get_npc_by_korean_name(target_name)
|
108 |
+
target_korean_name = target_npc.korean_name if target_npc else target_name
|
109 |
+
|
110 |
+
self_postposition = get_korean_postposition(self.owner_npc.korean_name, "은", "는")
|
111 |
+
target_postposition = get_korean_postposition(target_korean_name, "과", "와")
|
112 |
+
# 관계 요약도 간단하게 업데이트
|
113 |
+
profile.summary = f"{target_korean_name}{target_postposition} {self.owner_npc.korean_name}{self_postposition} {relationship_type} 관계이다."
|
114 |
+
|
115 |
+
simulation_core.add_log(f"[관계 설정] {self.owner_npc.korean_name} -> {target_korean_name} 관계가 '{relationship_type}'(으)로 설정되었습니다.")
|
116 |
+
|
117 |
def summarize_relationship(self, target_name: str, npc_manager: "NPCManager"):
|
118 |
""" LLM을 사용하여 특정 대상과의 관계를 주기적으로 요약하고 업데이트"""
|
119 |
from ..models.llm_helper import query_llm_with_prompt
|
npc_social_network/routes/npc_route.py
CHANGED
@@ -113,4 +113,27 @@ def get_npc_details(npc_name):
|
|
113 |
"goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "특별한 목표 없음",
|
114 |
"memories": [mem.content for mem in npc.memory_store.get_recent_memories(limit=10)]
|
115 |
}
|
116 |
-
return jsonify(details)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
"goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "특별한 목표 없음",
|
114 |
"memories": [mem.content for mem in npc.memory_store.get_recent_memories(limit=10)]
|
115 |
}
|
116 |
+
return jsonify(details)
|
117 |
+
|
118 |
+
@npc_bp.route("/api/force_relationship", methods=['POST'])
|
119 |
+
def force_relationship():
|
120 |
+
"""두 NPC의 관계를 강제로 설정하는 API"""
|
121 |
+
data = request.json
|
122 |
+
npc1_name = data.get("npc1_name")
|
123 |
+
npc2_name = data.get("npc2_name")
|
124 |
+
relationship_type = data.get("relationship_type")
|
125 |
+
|
126 |
+
if not all([npc1_name, npc2_name, relationship_type]) or npc1_name == npc2_name:
|
127 |
+
return jsonify({"success": False, "error": "Invalid data"}), 400
|
128 |
+
|
129 |
+
with simulation_core.simulation_lock:
|
130 |
+
npc1 = simulation_core.npc_manager.get_npc_by_korean_name(npc1_name)
|
131 |
+
npc2 = simulation_core.npc_manager.get_npc_by_korean_name(npc2_name)
|
132 |
+
|
133 |
+
if npc1 and npc2:
|
134 |
+
# 관계는 상호작용이므로, 양쪽 모두에게 관계를 설정
|
135 |
+
npc1.relationships.set_relationship(npc2.name, relationship_type)
|
136 |
+
npc2.relationships.set_relationship(npc1.name, relationship_type)
|
137 |
+
return jsonify({"success": True})
|
138 |
+
|
139 |
+
return jsonify({"success": False, "error": "NPC not found"}), 404
|
npc_social_network/templates/dashboard.html
CHANGED
@@ -26,6 +26,53 @@
|
|
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>
|
31 |
<body>
|
@@ -33,12 +80,35 @@
|
|
33 |
<div id="network-panel">
|
34 |
<div id="network"></div>
|
35 |
<div id="controls">
|
|
|
|
|
|
|
|
|
|
|
36 |
<button id="play-pause-btn">Play / Pause</button>
|
37 |
<button id="tick-btn">Next Tick</button>
|
38 |
<button id="refresh-btn">⟳ 새로고침</button>
|
39 |
-
|
40 |
-
<
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
</div>
|
43 |
</div>
|
44 |
<div id="side-panel">
|
@@ -65,6 +135,10 @@
|
|
65 |
const injectEventBtn = document.getElementById('inject-event-btn');
|
66 |
const eventNpcSelect = document.getElementById('event-npc-select');
|
67 |
const eventTextInput = document.getElementById('event-text-input');
|
|
|
|
|
|
|
|
|
68 |
|
69 |
let network = null;
|
70 |
let nodesDataSet = new vis.DataSet([]);
|
@@ -135,16 +209,18 @@
|
|
135 |
}
|
136 |
|
137 |
function updateNpcSelect(npcNodes) {
|
138 |
-
const
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
|
|
|
|
148 |
}
|
149 |
|
150 |
function updateLog(logMessages) {
|
@@ -163,6 +239,21 @@
|
|
163 |
autoUpdateInterval = null;
|
164 |
}
|
165 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
|
167 |
// 모든 이벤트 리스너 복구
|
168 |
playPauseBtn.addEventListener('click', async () => {
|
|
|
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 |
+
|
30 |
+
/* 하단 관계도 패널 배경 */
|
31 |
+
#controls {
|
32 |
+
background-color: #2c3e50;
|
33 |
+
}
|
34 |
+
|
35 |
+
/* 2. 탭 버튼 스타일 */
|
36 |
+
.control-tabs {
|
37 |
+
display: flex;
|
38 |
+
gap: 5px;
|
39 |
+
margin-bottom: 10px;
|
40 |
+
}
|
41 |
+
.tab-btn {
|
42 |
+
flex: 1;
|
43 |
+
padding: 10px;
|
44 |
+
cursor: pointer;
|
45 |
+
background-color: #e9ecef;
|
46 |
+
border: 1px solid #dee2e6;
|
47 |
+
border-bottom: none;
|
48 |
+
border-radius: 8px 8px 0 0;
|
49 |
+
font-weight: 500;
|
50 |
+
color: #495057;
|
51 |
+
}
|
52 |
+
.tab-btn.active {
|
53 |
+
background-color: #ffffff;
|
54 |
+
border-bottom: 1px solid #ffffff;
|
55 |
+
}
|
56 |
+
|
57 |
+
/* 탭 패널 스타일 */
|
58 |
+
.control-panel {
|
59 |
+
display: none; /* 기본적으로 모든 패널 숨김 */
|
60 |
+
flex-direction: column;
|
61 |
+
gap: 10px;
|
62 |
+
padding: 15px;
|
63 |
+
border: 1px solid #dee2e6;
|
64 |
+
border-radius: 0 0 8px 8px;
|
65 |
+
background-color: #ffffff;
|
66 |
+
}
|
67 |
+
|
68 |
+
.control-panel.active {
|
69 |
+
display: flex; /* 활성화된 패널만 보이도록 */
|
70 |
+
}
|
71 |
+
.control-panel select, .control-panel input {
|
72 |
+
width: 100%;
|
73 |
+
box-sizing: border-box;
|
74 |
+
}
|
75 |
+
|
76 |
</style>
|
77 |
</head>
|
78 |
<body>
|
|
|
80 |
<div id="network-panel">
|
81 |
<div id="network"></div>
|
82 |
<div id="controls">
|
83 |
+
<div class="control-tabs">
|
84 |
+
<button id="tab-event" class="tab-btn active">이벤트 주입</button>
|
85 |
+
<button id="tab-relation" class="tab-btn">관계 설정</button>
|
86 |
+
</div>
|
87 |
+
|
88 |
<button id="play-pause-btn">Play / Pause</button>
|
89 |
<button id="tick-btn">Next Tick</button>
|
90 |
<button id="refresh-btn">⟳ 새로고침</button>
|
91 |
+
|
92 |
+
<div id="event-inject-panel" class="control-panel active">
|
93 |
+
<select id="event-npc-select"></select>
|
94 |
+
<input type="text" id="event-text-input" placeholder="이벤트 내용">
|
95 |
+
<button id="inject-event-btn">이벤트 주입</button>
|
96 |
+
</div>
|
97 |
+
|
98 |
+
<div id="relationship-control-panel" class="control-panel">
|
99 |
+
<select id="relation-npc1-select"></select>
|
100 |
+
<select id="relation-npc2-select"></select>
|
101 |
+
<select id="relationship-type-select">
|
102 |
+
<option value="best friend">매우 친한 친구</option>
|
103 |
+
<option value="friend">친구</option>
|
104 |
+
<option value="acquaintance">지인</option>
|
105 |
+
<option value="stranger">낯선 사람</option>
|
106 |
+
<option value="nuisance">불편한 사람</option>
|
107 |
+
<option value="rival">싫은 사람</option>
|
108 |
+
<option value="enemy">적</option>
|
109 |
+
</select>
|
110 |
+
<button id="force-relationship-btn">관계 설정</button>
|
111 |
+
</div>
|
112 |
</div>
|
113 |
</div>
|
114 |
<div id="side-panel">
|
|
|
135 |
const injectEventBtn = document.getElementById('inject-event-btn');
|
136 |
const eventNpcSelect = document.getElementById('event-npc-select');
|
137 |
const eventTextInput = document.getElementById('event-text-input');
|
138 |
+
const relationNpc1Select = document.getElementById('relation-npc1-select');
|
139 |
+
const relationNpc2Select = document.getElementById('relation-npc2-select');
|
140 |
+
const relationshipTypeSelect = document.getElementById('relationship-type-select');
|
141 |
+
const forceRelationshipBtn = document.getElementById('force-relationship-btn');
|
142 |
|
143 |
let network = null;
|
144 |
let nodesDataSet = new vis.DataSet([]);
|
|
|
209 |
}
|
210 |
|
211 |
function updateNpcSelect(npcNodes) {
|
212 |
+
const selections = [eventNpcSelect, relationNpc1Select, relationNpc2Select];
|
213 |
+
selections.forEach(select => {
|
214 |
+
const currentVal = select.value;
|
215 |
+
select.innerHTML = '';
|
216 |
+
npcNodes.forEach(node => {
|
217 |
+
const option = document.createElement('option');
|
218 |
+
option.value = node.label;
|
219 |
+
option.textContent = node.label;
|
220 |
+
select.appendChild(option);
|
221 |
+
});
|
222 |
+
if (currentVal) select.value = currentVal;
|
223 |
+
})
|
224 |
}
|
225 |
|
226 |
function updateLog(logMessages) {
|
|
|
239 |
autoUpdateInterval = null;
|
240 |
}
|
241 |
}
|
242 |
+
|
243 |
+
forceRelationshipBtn.addEventListener('click', async () => {
|
244 |
+
const npc1_name = relationNpc1Select.value;
|
245 |
+
const npc2_name = relationNpc2Select.value;
|
246 |
+
const relationship_type = relationshipTypeSelect.value;
|
247 |
+
if (!npc1_name || !npc2_name || npc1_name === npc2_name) {
|
248 |
+
return alert('서로 다른 두 명의 NPC를 선택해주세요.');
|
249 |
+
}
|
250 |
+
await fetch('/npc_social_network/api/force_relationship', {
|
251 |
+
method: 'POST',
|
252 |
+
headers: { 'Content-Type': 'application/json' },
|
253 |
+
body: JSON.stringify({ npc1_name, npc2_name, relationship_type })
|
254 |
+
});
|
255 |
+
await updateWorld();
|
256 |
+
});
|
257 |
|
258 |
// 모든 이벤트 리스너 복구
|
259 |
playPauseBtn.addEventListener('click', async () => {
|