humanda5 commited on
Commit
04f338f
·
1 Parent(s): 8603187

멀티턴 대화 시스템 구현 - 작동 확인

Browse files
.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
- conv = self.active_conversation
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
- # LLM을 통해 전체 대화 내용을 문장으로 요약
73
- summary = summarize_memories(full_conversation)
 
74
 
75
- with simulation_core.simulation_lock:
76
- # 양쪽 모두에게 대화 기억을 저장
77
- initiator.remember(content=f"{target.korean_name}와 '{conv.topic}'에 대해 대화했다:\n{full_conversation}", importance=7, memory_type="Conversation")
78
- target.remember(content=f"{initiator.korean_name}와 '{conv.topic}'에 대해 대화했다:\n{full_conversation}", importance=7, memory_type="Conversation")
 
 
 
 
 
 
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", memories: List["Memory"]) -> dict:
 
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
- if conversation_manager and conversation_manager.is_conversation_active():
66
- conversation_manager.next_turn()
67
-
68
- elif not simulation_paused:
69
- tick_simulation()
70
-
71
- # 틱이 발생할 때마다 카운터 증가
72
- tick_counter += 1
73
- if tick_counter >= save_interval_ticks:
74
- with simulation_lock:
75
- save_simulation(npc_manager)
76
- tick_counter = 0
 
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