Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
04f338f
1
Parent(s):
8603187
멀티턴 대화 시스템 구현 - 작동 확인
Browse files- .vscode/launch.json +20 -0
- npc_social_network/data/saves/simulation_state.pkl +0 -0
- npc_social_network/data/vectorstores/bob.faiss +0 -0
- npc_social_network/data/vectorstores/charlie.faiss +0 -0
- npc_social_network/manager/conversation_manager.py +45 -23
- npc_social_network/models/llm_helper.py +33 -4
- npc_social_network/npc/npc_base.py +3 -2
- npc_social_network/simulation_core.py +14 -13
.vscode/launch.json
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
// Use IntelliSense to learn about possible attributes.
|
3 |
+
// Hover to view descriptions of existing attributes.
|
4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
5 |
+
"version": "0.2.0",
|
6 |
+
"configurations": [
|
7 |
+
{
|
8 |
+
"name": "Python Debugger: Debug app.py",
|
9 |
+
"type": "debugpy",
|
10 |
+
"request": "launch",
|
11 |
+
"program": "${workspaceFolder}/app.py",
|
12 |
+
"console": "integratedTerminal",
|
13 |
+
"env": {
|
14 |
+
"FLASK_NEV": "development",
|
15 |
+
"FLASK_APP": "app.py"
|
16 |
+
},
|
17 |
+
"args":[]
|
18 |
+
}
|
19 |
+
]
|
20 |
+
}
|
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/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/manager/conversation_manager.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1 |
# portfolio/npc_social_network/manager/conversation_manager.py
|
2 |
# 대화의 시작, 진행, 종료를 총괄하는 역할
|
3 |
from .. import simulation_core
|
4 |
-
from ..models.llm_helper import summarize_memories
|
|
|
5 |
from typing import List, Optional, TYPE_CHECKING
|
6 |
if TYPE_CHECKING:
|
7 |
from ..npc.npc_base import NPC
|
@@ -35,12 +36,31 @@ class ConversationManager:
|
|
35 |
def _add_and_log_utterance(self,speaker: "NPC", utterance: str):
|
36 |
"""대사를 대화 기록과 UI 로그에 모두 추가"""
|
37 |
if not self.is_conversation_active():
|
38 |
-
return
|
39 |
-
|
40 |
log_message = f"[{speaker.korean_name}]: {utterance}"
|
41 |
self.active_conversation.conversation_history.append(log_message)
|
42 |
simulation_core.add_log(log_message)
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
def start_conversation(self, initiator: "NPC", target: "NPC",
|
45 |
topic: Optional[str] = None):
|
46 |
"""새로운 대화를 시작, 바로 다음 턴을 호출"""
|
@@ -51,34 +71,33 @@ class ConversationManager:
|
|
51 |
self.active_conversation = Conversation(initiator, target, conv_topic)
|
52 |
print(f"--[대화 시작]--")
|
53 |
print(f"주제: {conv_topic} / 참여자: {initiator.korean_name}, {target.korean_name}")
|
54 |
-
|
55 |
-
|
56 |
|
57 |
# 1. 첫 마디 생성
|
58 |
self.next_turn()
|
59 |
|
60 |
def end_conversation(self, reason: str = "자연스럽게"):
|
61 |
"""현재 대화를 종료하고, 대화 내용을 요약하여 기억으로 저장"""
|
62 |
-
if self.is_conversation_active():
|
63 |
return
|
64 |
|
65 |
# 대화가 끝나면, 전체 대화 내용을 바탕으로 기억을 생성하고 관계를 업데이트
|
66 |
-
|
67 |
-
if conv.conversation_history:
|
68 |
-
initiator = conv.participants[0]
|
69 |
-
target = conv.participants[1]
|
70 |
-
full_conversation = "\n".join(conv.conversation_history)
|
71 |
|
72 |
-
|
73 |
-
|
|
|
74 |
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
-
print(f"---[대화 종료: {reason}]---\n")
|
81 |
-
self.active_conversation = None
|
82 |
|
83 |
def next_turn(self):
|
84 |
"""대화의 다음 턴을 진행, 생성된 대사 처리"""
|
@@ -86,17 +105,20 @@ class ConversationManager:
|
|
86 |
return
|
87 |
|
88 |
conv = self.active_conversation
|
|
|
89 |
|
|
|
90 |
if conv.is_ending:
|
|
|
|
|
91 |
self.end_conversation("작별 인사 완료")
|
92 |
return
|
93 |
-
|
94 |
-
speaker = conv.get_current_speaker()
|
95 |
|
96 |
-
#
|
97 |
utterance, action = speaker.generate_dialogue_turn(conv)
|
98 |
self._add_and_log_utterance(speaker, utterance)
|
99 |
|
|
|
100 |
if action != "CONTINUE" or len(conv.conversation_history) >= 10:
|
101 |
conv.is_ending = True
|
102 |
conv.switch_turn()
|
|
|
1 |
# portfolio/npc_social_network/manager/conversation_manager.py
|
2 |
# 대화의 시작, 진행, 종료를 총괄하는 역할
|
3 |
from .. import simulation_core
|
4 |
+
from ..models.llm_helper import summarize_memories, summarize_text
|
5 |
+
import threading
|
6 |
from typing import List, Optional, TYPE_CHECKING
|
7 |
if TYPE_CHECKING:
|
8 |
from ..npc.npc_base import NPC
|
|
|
36 |
def _add_and_log_utterance(self,speaker: "NPC", utterance: str):
|
37 |
"""대사를 대화 기록과 UI 로그에 모두 추가"""
|
38 |
if not self.is_conversation_active():
|
39 |
+
return
|
|
|
40 |
log_message = f"[{speaker.korean_name}]: {utterance}"
|
41 |
self.active_conversation.conversation_history.append(log_message)
|
42 |
simulation_core.add_log(log_message)
|
43 |
+
|
44 |
+
def _summarize_and_remember_in_background(self, conv: "Conversation"):
|
45 |
+
"""백그라운드 스레드 대화를 요약하고 기억시키는 함수"""
|
46 |
+
try:
|
47 |
+
if not conv.conversation_history:
|
48 |
+
print("기억할 대화 내용이 없어 종료.")
|
49 |
+
return
|
50 |
+
initiator, target = conv.participants[0], conv.participants[1]
|
51 |
+
full_conversation = "\n".join(conv.conversation_history)
|
52 |
+
|
53 |
+
summary = summarize_text(full_conversation)
|
54 |
+
with simulation_core.simulation_lock:
|
55 |
+
memory_content = f"'{conv.topic}'에 대해 대화하며 '{summary}'라는 결론을 내렸다."
|
56 |
+
initiator.remember(content=f"{target.korean_name}와(과) {memory_content}", importance=7, memory_type="Conversation")
|
57 |
+
target.remember(content=f"{initiator.korean_name}와(과) {memory_content}", importance=7, memory_type="Conversation")
|
58 |
+
print(f"[{initiator.korean_name}, {target.korean_name}] 대화 내용이 기억에 저장되었습니다.")
|
59 |
+
except Exception as e:
|
60 |
+
print(f"[에러] 백그라운드 기억 저장 중 문제 발생: {e}")
|
61 |
+
import traceback
|
62 |
+
traceback.print_exc()
|
63 |
+
|
64 |
def start_conversation(self, initiator: "NPC", target: "NPC",
|
65 |
topic: Optional[str] = None):
|
66 |
"""새로운 대화를 시작, 바로 다음 턴을 호출"""
|
|
|
71 |
self.active_conversation = Conversation(initiator, target, conv_topic)
|
72 |
print(f"--[대화 시작]--")
|
73 |
print(f"주제: {conv_topic} / 참여자: {initiator.korean_name}, {target.korean_name}")
|
|
|
|
|
74 |
|
75 |
# 1. 첫 마디 생성
|
76 |
self.next_turn()
|
77 |
|
78 |
def end_conversation(self, reason: str = "자연스럽게"):
|
79 |
"""현재 대화를 종료하고, 대화 내용을 요약하여 기억으로 저장"""
|
80 |
+
if not self.is_conversation_active():
|
81 |
return
|
82 |
|
83 |
# 대화가 끝나면, 전체 대화 내용을 바탕으로 기억을 생성하고 관계를 업데이트
|
84 |
+
conv_to_process = self.active_conversation
|
|
|
|
|
|
|
|
|
85 |
|
86 |
+
# 1. 대화 상태를 즉시 '종료'로 변경하여 메인 루프를 해방
|
87 |
+
self.active_conversation = None
|
88 |
+
print(f"---[대화 종료: {reason}]---\n")
|
89 |
|
90 |
+
# 2. 시간이 걸리는 '기억 저장' 작업은 별도의 스레드를 생성하여 백그라운드에서 처리.
|
91 |
+
background_thread = threading.Thread(
|
92 |
+
target=self._summarize_and_remember_in_background,
|
93 |
+
args=(conv_to_process,)
|
94 |
+
)
|
95 |
+
background_thread.start()
|
96 |
+
|
97 |
+
# 3. 대화가 종료된 직후, 현재까지의 시뮬레이션 전체 상태를 즉시 저장.
|
98 |
+
with simulation_core.simulation_lock:
|
99 |
+
simulation_core.save_simulation(simulation_core.npc_manager)
|
100 |
|
|
|
|
|
101 |
|
102 |
def next_turn(self):
|
103 |
"""대화의 다음 턴을 진행, 생성된 대사 처리"""
|
|
|
105 |
return
|
106 |
|
107 |
conv = self.active_conversation
|
108 |
+
speaker = conv.get_current_speaker()
|
109 |
|
110 |
+
# 상태 1: 마무�� 단계 (작별 인사)
|
111 |
if conv.is_ending:
|
112 |
+
utterance, _ = speaker.generate_dialogue_turn(conv, is_final_turn=True)
|
113 |
+
self._add_and_log_utterance(speaker, utterance)
|
114 |
self.end_conversation("작별 인사 완료")
|
115 |
return
|
|
|
|
|
116 |
|
117 |
+
# 상태 2: 진행 단계 (일반 대화)
|
118 |
utterance, action = speaker.generate_dialogue_turn(conv)
|
119 |
self._add_and_log_utterance(speaker, utterance)
|
120 |
|
121 |
+
# 상태 전환 결정
|
122 |
if action != "CONTINUE" or len(conv.conversation_history) >= 10:
|
123 |
conv.is_ending = True
|
124 |
conv.switch_turn()
|
npc_social_network/models/llm_helper.py
CHANGED
@@ -41,6 +41,23 @@ def query_llm_with_prompt(prompt: str) -> str:
|
|
41 |
return response.text.strip()
|
42 |
except Exception as e:
|
43 |
return f"[LLM Error] {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
def summarize_memories(memories: List[Memory]) -> str:
|
46 |
"""
|
@@ -179,11 +196,25 @@ Your response MUST be a valid JSON array of strings, where each string is a step
|
|
179 |
return result
|
180 |
return ["[계획 생성에 실패했습니다]"] # 최종 실패 시 안전한 값 반환
|
181 |
|
182 |
-
def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conversation",
|
|
|
183 |
"""NPC의 모든 내면 정보를 바탕으로 다음 대사와 행동을 생성."""
|
184 |
history_str = "\n".join(conversation.conversation_history)
|
185 |
memory_str = "\n".join([f"- {m.content}" for m in memories]) if memories else "관련된 특별한 기억 없음"
|
186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
# 대화 전략(Conversation Strategies) 프롬프트 도입
|
188 |
prompt = f"""
|
189 |
# Persona
|
@@ -203,11 +234,9 @@ def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conve
|
|
203 |
{history_str}
|
204 |
|
205 |
# Instruction
|
206 |
-
|
207 |
-
- 이 대화를 계속 이어갈지("CONTINUE"), 아니면 이 대사를 끝으로 대화를 마무리할지("END") 결정하세요.
|
208 |
- 당신의 응답은 반드시 아래 JSON 형식이어야 합니다.
|
209 |
- 대사에는 이름, 행동 묘사, 따옴표를 절대 포함하지 마세요.
|
210 |
-
- 단순히 상대방의 말을 따라 하거나, 계속해서 질문만 던지는 앵무새처럼 행동하지 마세요.
|
211 |
- 아래의 'Conversation Strategies'과 같이 현재 대화의 맥락과 당신의 성격에 가장 적절한 행동 하나를 선택하세요.
|
212 |
|
213 |
# Conversation Strategies
|
|
|
41 |
return response.text.strip()
|
42 |
except Exception as e:
|
43 |
return f"[LLM Error] {str(e)}"
|
44 |
+
|
45 |
+
def summarize_text(text: str) -> str:
|
46 |
+
"""주어진 긴 텍스트(대화 내용 등)를 한 문장으로 요약"""
|
47 |
+
if not text.strip():
|
48 |
+
return "아무 내용 없는 대화였음"
|
49 |
+
|
50 |
+
prompt = f"""
|
51 |
+
# Instruction
|
52 |
+
다음 텍스트를 가장 핵심적인 내용을 담아 한국어 한 문장으로 요약해주세요.
|
53 |
+
|
54 |
+
# Text
|
55 |
+
{text}
|
56 |
+
|
57 |
+
# Summary (a single sentence):
|
58 |
+
"""
|
59 |
+
summary = query_llm_with_prompt(prompt)
|
60 |
+
return summary if summary and "[LLM Error]" not in summary else "대화 요약에 실패함"
|
61 |
|
62 |
def summarize_memories(memories: List[Memory]) -> str:
|
63 |
"""
|
|
|
196 |
return result
|
197 |
return ["[계획 생성에 실패했습니다]"] # 최종 실패 시 안전한 값 반환
|
198 |
|
199 |
+
def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conversation",
|
200 |
+
memories: List["Memory"], is_final_turn: bool = False) -> dict:
|
201 |
"""NPC의 모든 내면 정보를 바탕으로 다음 대사와 행동을 생성."""
|
202 |
history_str = "\n".join(conversation.conversation_history)
|
203 |
memory_str = "\n".join([f"- {m.content}" for m in memories]) if memories else "관련된 특별한 기억 없음"
|
204 |
|
205 |
+
if is_final_turn:
|
206 |
+
task_instruction = """
|
207 |
+
- 상대방이 대화를 마무리하려고 합니다. 상대방의 마지막 말에 어울리는 자연스러운 작별 인사를 생성하세요. (예: "네, 조심히 가세요.", "다음에 또 봬요.", "알겠습니다. 좋은 하루 보내세요.")
|
208 |
+
- 이 대사를 끝으로 대화를 마무리("END")하세요.
|
209 |
+
"""
|
210 |
+
else:
|
211 |
+
task_instruction = """
|
212 |
+
- 위 대화의 흐름과 주제에 맞춰, 당신이 이번 턴에 할 자연스러운 대사를 생성하세요.
|
213 |
+
- 대화를 계속 이어갈지("CONTINUE"), 아니면 이 대사를 끝으로 대화를 마무리할지("END") 결정하세요.
|
214 |
+
- **대화를 끝내기로 결정했다면 (`action: "END"`), 당신의 대사는 반드시 "이제 가봐야겠어요", "다음에 또 이야기 나눠요" 와 같이 대화의 마무리를 암시하는 내용이어야 합니다.**
|
215 |
+
- 단순히 상대방의 말에 동의만 하지 말고, 당신의 기억이나 성격을 바탕으로 새로운 생각이나 질문, 화제를 꺼내어 대화를 풍부하게 만드세요.
|
216 |
+
"""
|
217 |
+
|
218 |
# 대화 전략(Conversation Strategies) 프롬프트 도입
|
219 |
prompt = f"""
|
220 |
# Persona
|
|
|
234 |
{history_str}
|
235 |
|
236 |
# Instruction
|
237 |
+
{task_instruction}
|
|
|
238 |
- 당신의 응답은 반드시 아래 JSON 형식이어야 합니다.
|
239 |
- 대사에는 이름, 행동 묘사, 따옴표를 절대 포함하지 마세요.
|
|
|
240 |
- 아래의 'Conversation Strategies'과 같이 현재 대화의 맥락과 당신의 성격에 가장 적절한 행동 하나를 선택하세요.
|
241 |
|
242 |
# Conversation Strategies
|
npc_social_network/npc/npc_base.py
CHANGED
@@ -142,7 +142,7 @@ class NPC:
|
|
142 |
self.personality_stage = None
|
143 |
self.update_personality_stage()
|
144 |
|
145 |
-
def generate_dialogue_turn(self, conversation: "Conversation") -> Tuple[str, str]:
|
146 |
"""대화의 현재 턴에 대한 응답과 행동을 생성 (기억과 관계를 총 동원)"""
|
147 |
|
148 |
# 1. 생각하기: '첫 마디'와 '대답'에 따라 다른 정보로 기억을 탐색.
|
@@ -158,7 +158,8 @@ class NPC:
|
|
158 |
speaker = self,
|
159 |
target = conversation.participants[1-conversation.turn_index],
|
160 |
conversation = conversation,
|
161 |
-
memories = relevant_memories
|
|
|
162 |
)
|
163 |
dialogue = result.get("dialogue", "...")
|
164 |
action = result.get("action", "END").strip().upper()
|
|
|
142 |
self.personality_stage = None
|
143 |
self.update_personality_stage()
|
144 |
|
145 |
+
def generate_dialogue_turn(self, conversation: "Conversation", is_final_turn: bool=False) -> Tuple[str, str]:
|
146 |
"""대화의 현재 턴에 대한 응답과 행동을 생성 (기억과 관계를 총 동원)"""
|
147 |
|
148 |
# 1. 생각하기: '첫 마디'와 '대답'에 따라 다른 정보로 기억을 탐색.
|
|
|
158 |
speaker = self,
|
159 |
target = conversation.participants[1-conversation.turn_index],
|
160 |
conversation = conversation,
|
161 |
+
memories = relevant_memories,
|
162 |
+
is_final_turn = is_final_turn
|
163 |
)
|
164 |
dialogue = result.get("dialogue", "...")
|
165 |
action = result.get("action", "END").strip().upper()
|
npc_social_network/simulation_core.py
CHANGED
@@ -61,19 +61,20 @@ def simulation_loop():
|
|
61 |
tick_counter = 0
|
62 |
|
63 |
while True:
|
64 |
-
|
65 |
-
|
66 |
-
conversation_manager.
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
77 |
|
78 |
time.sleep(5) # 5초에 한 번씩 틱 발생 (이것보다 줄이면 Gemini API 사용량 초과 발생 가능)
|
79 |
|
|
|
61 |
tick_counter = 0
|
62 |
|
63 |
while True:
|
64 |
+
if not simulation_paused:
|
65 |
+
# 대화 중일 때는 대화만 진행
|
66 |
+
if conversation_manager and conversation_manager.is_conversation_active():
|
67 |
+
conversation_manager.next_turn()
|
68 |
+
|
69 |
+
# 대화 중이 아닐때 전체 시뮬레이션 진행
|
70 |
+
else:
|
71 |
+
tick_simulation()
|
72 |
+
# 틱이 발생할 때마다 카운터 증가
|
73 |
+
tick_counter += 1
|
74 |
+
if tick_counter >= save_interval_ticks:
|
75 |
+
with simulation_lock:
|
76 |
+
save_simulation(npc_manager)
|
77 |
+
tick_counter = 0
|
78 |
|
79 |
time.sleep(5) # 5초에 한 번씩 틱 발생 (이것보다 줄이면 Gemini API 사용량 초과 발생 가능)
|
80 |
|