File size: 11,237 Bytes
7cfe55a
dde6bd9
 
b03bfdc
1a6fb12
f1c565e
dde6bd9
7cfe55a
a3830f2
 
 
b03bfdc
 
6fe0d0c
7cfe55a
dde6bd9
 
 
73db488
b03bfdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6fe0d0c
dde6bd9
b03bfdc
 
f1c565e
 
 
 
 
 
 
 
 
b03bfdc
1a6fb12
 
 
 
f1c565e
 
 
d57920f
 
f1c565e
 
 
 
 
 
 
 
 
d57920f
 
dde6bd9
f1c565e
 
 
 
 
 
 
 
 
 
 
dde6bd9
f1c565e
1a6fb12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9df6a7e
dde6bd9
 
6fe0d0c
 
 
 
 
 
dde6bd9
 
 
 
 
 
 
 
 
 
 
 
 
3e5e60d
dde6bd9
 
 
6fe0d0c
d57920f
6fe0d0c
3e5e60d
 
6fe0d0c
 
 
 
 
 
 
d57920f
6fe0d0c
 
 
 
d57920f
d505821
6fe0d0c
 
 
 
2cd8c90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09e5b85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a6fb12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00b7ce9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# portfolio/npc_social_network/routes/npc_route.py
from flask import Blueprint, render_template, request, jsonify, url_for
from .. import simulation_core
from ..models.gemini_setup import initialize_model
import os



npc_bp = Blueprint(
    "npc_social",
    __name__,
    static_folder="../static",
    static_url_path="/npc_social_network"
)

@npc_bp.route("/")
def dashboard():
    return render_template("dashboard.html")

# ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์‹œ์ž‘ํ•˜๋Š” API
@npc_bp.route("/api/initialize_simulation", methods=['POST'])
def api_initialize_simulation():
    """API ํ‚ค๋ฅผ ๋ฐ›์•„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์‹œ์ž‘ํ•˜๋Š” API"""
    from ..models.gemini_setup import initialize_model
    data = request.json
    model_name = data.get("model_name", "gemini-2.0-flash")
    api_key = data.get("api_key")

    if not api_key:
        return jsonify({"success": False, "error": "API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."}), 400
    
    # 1. ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹œ๋„
    new_model = initialize_model(model_name, api_key)
    
    if new_model:
        # 2. ์„ฑ๊ณต์‹œ, ์ „์—ญ ๋ชจ๋ธ์„ ์„ค์ •ํ•˜๊ณ  ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์‹œ์ž‘
        simulation_core.active_llm_model = new_model
        if not simulation_core.simulation_initialized:
            simulation_core.initialize_simulation()
            simulation_core.start_simulation_loop()
        return jsonify({"success": True, "message": f"'{model_name}' ๋ชจ๋ธ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค."})
    else:
        return jsonify({"success": False, "error": "API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ชจ๋ธ ์ดˆ๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}), 400

@npc_bp.route("/api/world_state", methods=['GET'])
def get_world_state():
    if not simulation_core.simulation_initialized:
        return jsonify({"status": "needs_initialization", "log": simulation_core.event_log})
    with simulation_core.simulation_lock:
        if not simulation_core.npc_manager:
            return jsonify({"error": "Simulation not started"}), 500
        all_npcs = list(simulation_core.npc_manager.all())
        current_log = list(simulation_core.event_log)
        is_paused = simulation_core.simulation_paused

    nodes = []
    for npc in all_npcs:
        # ๊ฐ NPC์˜ ์˜์–ด ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋ช…์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: elin -> elin.png)
        image_filename = f"images/npc/{npc.name}.png"
        # if not os.path.exists(image_filename):
        #     image_filename = f"images/npc/npc.png"


        nodes.append({
            # id๋Š” ์˜์–ด๋กœ, label์€ ํ•œ๊ธ€๋กœ ๋ถ„๋ฆฌ
            "id": npc.name,
            "label": npc.korean_name,
            "shape": "image",  # ๋…ธ๋“œ ๋ชจ์–‘์„ 'image'๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
            "image": url_for('npc_social.static', filename=image_filename), # ์˜ฌ๋ฐ”๋ฅธ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
            "size": 40 # ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋ฅผ ์ ์ ˆํžˆ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.
        })

    edges = []
    drawn_relations = set()
    for npc in all_npcs:
        for target_name, profile in npc.relationships.relationships.items():
            from_id = npc.name
            to_id = target_name
            
            relation_pair = tuple(sorted((from_id, to_id)))
            if relation_pair in drawn_relations: continue

            color = {"best friend": "#00E676", "friend": "#4CAF50", "acquaintance": "#81D4FA",
                     "nuisance": "#FFC107", "rival": "#FF9800", "enemy": "#F44336",
                     "stranger":"#BDBDBD"}.get(profile.type, "gray")
            
            edges.append({
                "from": from_id, "to": to_id,
                "label": f"{profile.type} ({profile.score:.1f})",
                "color": color
            })
            drawn_relations.add(relation_pair)
    
    # ํ”Œ๋ ˆ์ด์–ด ์ž…๋ ฅ ๋Œ€๊ธฐ ์ƒํƒœ ์ •๋ณด
    waiting_for_player = False
    player_conversation_info = None
    if simulation_core.conversation_manager and simulation_core.conversation_manager.is_conversation_active():
        conv = simulation_core.conversation_manager.active_conversation
        if conv.waiting_for_player:
            waiting_for_player = True
            # ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ ๋ง์„ ๊ฑด NPC์˜ ์ •๋ณด ์ฐพ๊ธฐ
            other_npc = conv.participants[1 - conv.turn_index]
            player_conversation_info = {
                "npc_name": other_npc.korean_name,
                "last_utterance": conv.conversation_history[-1] if conv.conversation_history else ""
            }

    return jsonify({ "nodes": nodes, "edges": edges, "log": current_log, "paused": is_paused, "waiting_for_player": waiting_for_player, "player_conversation": player_conversation_info })

@npc_bp.route("/api/toggle_simulation", methods=['POST'])
def toggle_simulation():
    with simulation_core.simulation_lock:
        simulation_core.simulation_paused = not simulation_core.simulation_paused
        status = "์ •์ง€๋จ" if simulation_core.simulation_paused else "์‹คํ–‰ ์ค‘"
        simulation_core.add_log(f"์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด {status} ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
        is_paused = simulation_core.simulation_paused
    return jsonify({"paused": is_paused})

@npc_bp.route("/api/manual_tick", methods=['POST'])
def manual_tick():
    # ์ˆ˜๋™ ํ‹ฑ์€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ๋ฉˆ์ถฐ์žˆ์„ ๋•Œ๋งŒ ์ž‘๋™ํ•˜๋„๋ก ํ•จ
    if simulation_core.simulation_paused:
        simulation_core.tick_simulation()
    else:
        simulation_core.add_log("๊ฒฝ๊ณ : ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ์‹คํ–‰ ์ค‘์ผ ๋•Œ๋Š” ์ˆ˜๋™ ํ‹ฑ์„ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
    return get_world_state()


@npc_bp.route("/api/inject_event", methods=['POST'])
def inject_event():
    """ํ”Œ๋ ˆ์ด์–ด๊ฐ€ NPC์—๊ฒŒ '๊ณผ๊ฑฐ ๊ธฐ์–ต'์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฃผ์ž…ํ•˜๋Š” ํ•จ์ˆ˜"""
    data = request.json
    npc_name, event_text = data.get("npc_name"), data.get("event_text")
    
    with simulation_core.simulation_lock:
        npc = simulation_core.npc_manager.get_npc_by_korean_name(npc_name)
        if npc and event_text:
            npc.remember(content=event_text, importance=8, emotion="nostalgia", memory_type="Recalled_Past")      # ์ด๋ฒคํŠธ ํƒ€์ž…: ์ƒ๊ธฐ๋œ ๊ณผ๊ฑฐ
            simulation_core.add_log(f"ํ”Œ๋ ˆ์ด์–ด ๊ฐœ์ž… -> {npc_name}์—๊ฒŒ '์ƒ๊ธฐ๋œ ๊ณผ๊ฑฐ' ์ฃผ์ž…: '{event_text}'")
            return jsonify({"success": True})
    return jsonify({"success": False, "error": "Invalid data"}), 400

# get_npc_details ์™€ ๊ฐ™์€ ๋‚˜๋จธ์ง€ ์ฝ๊ธฐ ์ „์šฉ API๋„ Lock์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
@npc_bp.route("/api/npc_details/<npc_name>", methods=['GET'])
def get_npc_details(npc_name):
    with simulation_core.simulation_lock:
        npc = simulation_core.npc_manager.get_npc_by_korean_name(npc_name)
        if not npc:
            return jsonify({"error": "NPC not found"}), 404

        details = {
            "name": npc.korean_name, "age": npc.age, "job": npc.job,
            "personality_summary": npc.personality.get_narrative_summary(),
            "emotions": npc.get_composite_emotion_state(top_n=5),
            "goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "ํŠน๋ณ„ํ•œ ๋ชฉํ‘œ ์—†์Œ",
            "memories": [mem.content for mem in npc.memory_store.get_recent_memories(limit=10)]
        }
    return jsonify(details)

@npc_bp.route("/api/force_relationship", methods=['POST'])
def force_relationship():
    """๋‘ NPC์˜ ๊ด€๊ณ„๋ฅผ ๊ฐ•์ œ๋กœ ์„ค์ •ํ•˜๋Š” API"""
    data = request.json
    npc1_name = data.get("npc1_name")
    npc2_name = data.get("npc2_name")
    relationship_type = data.get("relationship_type")

    if not all([npc1_name, npc2_name, relationship_type]) or npc1_name == npc2_name:
        return jsonify({"success": False, "error": "Invalid data"}), 400
    
    with simulation_core.simulation_lock:
        npc1 = simulation_core.npc_manager.get_npc_by_korean_name(npc1_name)
        npc2 = simulation_core.npc_manager.get_npc_by_korean_name(npc2_name)

        if npc1 and npc2:
            # ๊ด€๊ณ„๋Š” ์ƒํ˜ธ์ž‘์šฉ์ด๋ฏ€๋กœ, ์–‘์ชฝ ๋ชจ๋‘์—๊ฒŒ ๊ด€๊ณ„๋ฅผ ์„ค์ •
            npc1.relationships.set_relationship(npc2.name, relationship_type)
            npc2.relationships.set_relationship(npc1.name, relationship_type)
            return jsonify({"success": True})
        
    return jsonify({"success": False, "error": "NPC not found"}), 404

@npc_bp.route("/api/orchestrate_conversation", methods=['Post'])
def orchestrate_conversation():
    """ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ง€์ •ํ•œ ๋‘ NPC์™€ ์ƒํ™ฉ์œผ๋กœ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘์‹œํ‚ค๋Š” API"""
    data = request.json
    npc1_name = data.get("npc1_name")
    npc2_name = data.get("npc2_name")
    situation = data.get("situation")   # ๋Œ€ํ™” ํ•ต์‹ฌ 'topic'

    if not all([npc1_name, npc2_name, situation]) or npc1_name == npc2_name:
        return jsonify({"success": False, "error": "Invalid data"}), 400
    
    # ๋ฝ์„ ๊ฑธ์ง€ ์•Š๊ณ  ๋งค๋‹ˆ์ €์— ๋ฐ”๋กœ ์š”์ฒญ
    npc1 = simulation_core.npc_manager.get_npc_by_korean_name(npc1_name)
    npc2 = simulation_core.npc_manager.get_npc_by_korean_name(npc2_name)

    if npc1 and npc2:
        # ๊ธฐ์กด์˜ ๋Œ€ํ™” ์‹œ์ž‘ ํ•จ์ˆ˜ ์žฌํ™œ์šฉ
        simulation_core.conversation_manager.start_conversation(npc1, npc2, topic=situation)
        return jsonify({"success": True})
    
    return jsonify({"success": False, "error": "NPC not found"}), 404

@npc_bp.route("/api/player_response", methods=['POST'])
def player_response():
    """ํ”Œ๋ ˆ์ด์–ด์˜ ๋Œ€ํ™” ์‘๋‹ต์„ ๋ฐ›์•„ '๋Œ€๊ธฐ์—ด'์— ์ถ”๊ฐ€, ์ฒ˜๋ฆฌ๋Š” simulation_loop์—์„œ ์ง„ํ–‰"""
    data = request.json
    utterance = data.get("utterance")

    if not utterance:
        return jsonify({"success": False, "error": "Invalid data"}), 400
    
    simulation_core.set_player_utterance(utterance)

    return jsonify({"success": True})

@npc_bp.route("/api/toggle_player", methods = ['POST'])
def toggle_player():
    """ํ”Œ๋ ˆ์ด์–ด์˜ ํ™œ์„ฑํ™” ์ƒํƒœ๋ฅผ ํ† ๊ธ€ํ•˜๋Š” API"""
    with simulation_core.simulation_lock:
        # npc_manager์˜ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝ
        simulation_core.npc_manager.set_player_active(not simulation_core.npc_manager.player_is_active)
        is_active = simulation_core.npc_manager.player_is_active
        status_text = "ํ™œ์„ฑํ™”" if is_active else "๋น„ํ™œ์„ฑํ™”"
        simulation_core.add_log(f"ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ๊ฐ€ '{status_text}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    return jsonify({"success": True, "player_is_active": is_active})

@npc_bp.route("/api/set_llm_config", methods=['POST'])
def set_llm_config():
    """UI๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ๋ชจ๋ธ๊ณผ API ํ‚ค๋กœ LLM์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์žฌ์„ค์ •ํ•˜๋Š” API"""
    from ..models.gemini_setup import initialize_model
    
    data = request.json
    model_name = data.get("model_name")
    api_key = data.get("api_key")

    if not all([model_name, api_key]):
        return jsonify({"success": False, "error": "๋ชจ๋ธ๊ณผ API ํ‚ค๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."}), 400

    # ์ƒˆ๋กœ์šด ์„ค์ •์œผ๋กœ ๋ชจ๋ธ์„ ์ดˆ๊ธฐํ™” ์‹œ๋„
    new_model = initialize_model(model_name, api_key)

    if new_model:
        # ์„ฑ๊ณต ์‹œ, simulation_core์˜ ์ „์—ญ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ์ƒˆ๋กœ์šด ๋ชจ๋ธ๋กœ ๊ต์ฒด
        simulation_core.active_llm_model = new_model
        return jsonify({"success": True})
    else:
        return jsonify({"success": False, "error": "API ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ชจ๋ธ ์ดˆ๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}), 400