Spaces:
Sleeping
Sleeping
humanda5
commited on
Commit
·
9df6a7e
1
Parent(s):
1ab2ba0
에러 수정 및 리팩토링
Browse files- npc_social_network/README.txt +13 -13
- npc_social_network/maps/engine/game_engine.py +1 -1
- npc_social_network/models/llm_helper.py +29 -30
- npc_social_network/models/llm_prompt_builder.py +49 -16
- npc_social_network/npc/emotion_config.py +36 -12
- npc_social_network/npc/npc_base.py +143 -158
- npc_social_network/npc/npc_memory_embedder.py +10 -16
- npc_social_network/npc/npc_relationship.py +20 -6
- npc_social_network/routes/npc_route.py +6 -3
- run_npc_interactions.py +34 -9
- test.ipynb +133 -0
npc_social_network/README.txt
CHANGED
@@ -117,7 +117,7 @@ portfolio/
|
|
117 |
|
118 |
## 🔁 주요 시퀀스 흐름
|
119 |
|
120 |
-
### 🗨️ `
|
121 |
1. LLM으로 감정 추론 (`query_llm_for_emotion`)
|
122 |
2. 기억 검색 (FAISS + embedding)
|
123 |
3. LLM 프롬프트 생성 (`build_npc_prompt`)
|
@@ -152,7 +152,7 @@ GET /run_npc_simulation
|
|
152 |
| 2단계 | NPC 감정 상태 및 감정 HUD 구현 | ✅ 완료 | 감정 상태 및 HUD 표시 (Pygame + 웹)
|
153 |
| 3단계 | 감정 ↔ 행동 연동 구조 구현 | ✅ 완료 | 감정 ↔ 행동 매핑 구현
|
154 |
| 4단계 | 감정 ↔ 기억 저장 및 decay 구현 | ✅ 완료 | Memory 감정 및 decay 적용 구조
|
155 |
-
| 5단계 | 감정 ↔ 관계 변화 구조 구현 | 🔸 부분 완료 | 구조는 구현됨,
|
156 |
| 6단계 | 성격 시스템 도입 | ✅ 완료 | 성격 분리 + 초기 bias 설계
|
157 |
| 7단계 | NPC 간 상호작용 구조화 | ✅ 완료 | NPC 상호작용 → 감정/관계 반영
|
158 |
| 8단계 | Memory 임베딩 + FAISS 기반 검색 | ✅ 완료 | FAISS 검색 + 임베딩 저장 구조
|
@@ -160,7 +160,7 @@ GET /run_npc_simulation
|
|
160 |
| 10단계 | 감정 상태 HUD 시각화 (웹 + 게임 화면) | ✅ 완료 | 감정 상태 HUD (웹/게임 연동)
|
161 |
|
162 |
# 핵심 시스템 고도화
|
163 |
-
| 11단계 | 감정/기억/관계 통합 정비 |
|
164 |
| 12단계 | LLM 대사 생성 고도화 | ✅ 완료 | LLM 대사 생성 개선, 프롬프트 완성
|
165 |
| 13단계 | 기억 decay 정교화 | ⏳ 미진행 | decay 시점별 변화율 조정 등 정교화 미실행
|
166 |
| 14-1단계 | 기억 임베딩 구조 확장 | ✅ 완료 | Memory 객체 임베딩 구조 도입
|
@@ -173,16 +173,16 @@ GET /run_npc_simulation
|
|
173 |
| 20단계 | NPC 사회 네트워크 시각화 | ⏳ 미진행 |
|
174 |
|
175 |
# 고급 인간화 및 자각형 NPC 확장
|
176 |
-
| 21단계 | 성격 지속적 변화 설계 완성 |
|
177 |
-
| 22단계 | 행동 유도 기반 계획형 NPC |
|
178 |
-
| 23단계 | 윤리 시스템 실험 (자각형 구조화) |
|
179 |
-
| 24단계 | NPC 자아 형성 시퀀스 테스트 |
|
180 |
-
| 25단계 | 플레이어 기억 및 반응 시스템 구축 |
|
181 |
-
| 26단계 | 멀티캐릭터 다자 간 상호작용 루프 |
|
182 |
-
| 27단계 | 시뮬레이션 UI 시각화 고도화 |
|
183 |
-
| 28단계 | 메타기억 시스템 |
|
184 |
-
| 29단계 | LLM+RAG+플래너 종합 구조 통합 |
|
185 |
-
| 30단계 | 논문/연구 결과 정리 구조화 |
|
186 |
|
187 |
|
188 |
---
|
|
|
117 |
|
118 |
## 🔁 주요 시퀀스 흐름
|
119 |
|
120 |
+
### 🗨️ `generate_dialogue()` 전체 흐름
|
121 |
1. LLM으로 감정 추론 (`query_llm_for_emotion`)
|
122 |
2. 기억 검색 (FAISS + embedding)
|
123 |
3. LLM 프롬프트 생성 (`build_npc_prompt`)
|
|
|
152 |
| 2단계 | NPC 감정 상태 및 감정 HUD 구현 | ✅ 완료 | 감정 상태 및 HUD 표시 (Pygame + 웹)
|
153 |
| 3단계 | 감정 ↔ 행동 연동 구조 구현 | ✅ 완료 | 감정 ↔ 행동 매핑 구현
|
154 |
| 4단계 | 감정 ↔ 기억 저장 및 decay 구현 | ✅ 완료 | Memory 감정 및 decay 적용 구조
|
155 |
+
| 5단계 | 감정 ↔ 관계 변화 구조 구현 | 🔸 부분 완료 | 구조는 구현됨, 다른 NPC 관련 확인 필요
|
156 |
| 6단계 | 성격 시스템 도입 | ✅ 완료 | 성격 분리 + 초기 bias 설계
|
157 |
| 7단계 | NPC 간 상호작용 구조화 | ✅ 완료 | NPC 상호작용 → 감정/관계 반영
|
158 |
| 8단계 | Memory 임베딩 + FAISS 기반 검색 | ✅ 완료 | FAISS 검색 + 임베딩 저장 구조
|
|
|
160 |
| 10단계 | 감정 상태 HUD 시각화 (웹 + 게임 화면) | ✅ 완료 | 감정 상태 HUD (웹/게임 연동)
|
161 |
|
162 |
# 핵심 시스템 고도화
|
163 |
+
| 11단계 | 감정/기억/관계 통합 정비 | ✅ 완료 | 관계 통합, 감정-기억-관계 연결 완성도 향상 중
|
164 |
| 12단계 | LLM 대사 생성 고도화 | ✅ 완료 | LLM 대사 생성 개선, 프롬프트 완성
|
165 |
| 13단계 | 기억 decay 정교화 | ⏳ 미진행 | decay 시점별 변화율 조정 등 정교화 미실행
|
166 |
| 14-1단계 | 기억 임베딩 구조 확장 | ✅ 완료 | Memory 객체 임베딩 구조 도입
|
|
|
173 |
| 20단계 | NPC 사회 네트워크 시각화 | ⏳ 미진행 |
|
174 |
|
175 |
# 고급 인간화 및 자각형 NPC 확장
|
176 |
+
| 21단계 | 성격 지속적 변화 설계 완성 | ⏳ 미진행 |
|
177 |
+
| 22단계 | 행동 유도 기반 계획형 NPC | ⏳ 미진행 |
|
178 |
+
| 23단계 | 윤리 시스템 실험 (자각형 구조화) | ⏳ 미진행 |
|
179 |
+
| 24단계 | NPC 자아 형성 시퀀스 테스트 | ⏳ 미진행 |
|
180 |
+
| 25단계 | 플레이어 기억 및 반응 시스템 구축 | ⏳ 미진행 |
|
181 |
+
| 26단계 | 멀티캐릭터 다자 간 상호작용 루프 | ⏳ 미진행 |
|
182 |
+
| 27단계 | 시뮬레이션 UI 시각화 고도화 | ⏳ 미진행 |
|
183 |
+
| 28단계 | 메타기억 시스템 | ⏳ 미진행 |
|
184 |
+
| 29단계 | LLM+RAG+플래너 종합 구조 통합 | ⏳ 미진행 |
|
185 |
+
| 30단계 | 논문/연구 결과 정리 구조화 | ⏳ 미진행 |
|
186 |
|
187 |
|
188 |
---
|
npc_social_network/maps/engine/game_engine.py
CHANGED
@@ -142,7 +142,7 @@ class GameEngine:
|
|
142 |
if npc:
|
143 |
print(f"\n👤 {npc.name} ({npc.job})")
|
144 |
print(f" - 위치: ({tile_x}, {tile_y})")
|
145 |
-
print(f" - 대화: {npc.generate_dialogue()}")
|
146 |
print(f" - 기억: {npc.recall()}")
|
147 |
self.set_selected_npc(npc)
|
148 |
|
|
|
142 |
if npc:
|
143 |
print(f"\n👤 {npc.name} ({npc.job})")
|
144 |
print(f" - 위치: ({tile_x}, {tile_y})")
|
145 |
+
print(f" - 대화: {npc.generate_dialogue(user_input='', use_llm=False)}")
|
146 |
print(f" - 기억: {npc.recall()}")
|
147 |
self.set_selected_npc(npc)
|
148 |
|
npc_social_network/models/llm_helper.py
CHANGED
@@ -1,23 +1,12 @@
|
|
1 |
# npc_social_network/models/llm_helper.py
|
2 |
|
3 |
from npc_social_network.npc.emotion_config import EMOTION_LIST
|
|
|
|
|
|
|
4 |
|
5 |
-
#
|
6 |
-
|
7 |
-
"""
|
8 |
-
LLM을 통해 NPC 응답 생성
|
9 |
-
"""
|
10 |
-
|
11 |
-
# 실제 구현시 Gemini API 호출 코드 사용 가능
|
12 |
-
prompt = f"""
|
13 |
-
NPC 이름: {npc_name}
|
14 |
-
NPC 직업: {npc_job}
|
15 |
-
플레이어가 다음과 같이 말했습니다. "{user_input}"
|
16 |
-
|
17 |
-
이에 대해 자연스럽고 친근한 한국어로 응답을 작성하세요.
|
18 |
-
"""
|
19 |
-
# 예시 응답 (추후 LLM 연결 시 교체)
|
20 |
-
return f"{npc_name}이(가) 대답했습니다: '네, 정말 흥미롭네요!'"
|
21 |
|
22 |
def query_llm_for_emotion(user_input):
|
23 |
"""
|
@@ -31,29 +20,39 @@ def query_llm_for_emotion(user_input):
|
|
31 |
|
32 |
반드시 감정 이름을 한 개만 출력하세요. (예: joy)
|
33 |
"""
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
36 |
|
37 |
def query_llm_with_prompt(prompt: str) -> str:
|
38 |
"""
|
39 |
prompt 문자열을 받아 LLM 호출
|
40 |
"""
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
45 |
|
46 |
-
def summarize_memories(
|
47 |
"""
|
48 |
-
|
49 |
"""
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
|
53 |
{joined}
|
54 |
|
55 |
-
→ 이 사람은 어떤 경험을 했는지 1~2문장으로 요약해 주세요.
|
56 |
-
"""
|
57 |
|
58 |
-
|
59 |
-
|
|
|
|
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 |
|
8 |
+
# Gemini 모델 초기화
|
9 |
+
gemini_model = load_gemini()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
def query_llm_for_emotion(user_input):
|
12 |
"""
|
|
|
20 |
|
21 |
반드시 감정 이름을 한 개만 출력하세요. (예: joy)
|
22 |
"""
|
23 |
+
try:
|
24 |
+
response = gemini_model.generate_content(prompt)
|
25 |
+
return response.text.strip()
|
26 |
+
except Exception as e:
|
27 |
+
return f"[LLM Error] {str(e)}"
|
28 |
|
29 |
def query_llm_with_prompt(prompt: str) -> str:
|
30 |
"""
|
31 |
prompt 문자열을 받아 LLM 호출
|
32 |
"""
|
33 |
+
try:
|
34 |
+
response = gemini_model.generate_content(prompt)
|
35 |
+
return response.text.strip()
|
36 |
+
except Exception as e:
|
37 |
+
return f"[LLM Error] {str(e)}"
|
38 |
|
39 |
+
def summarize_memories(memories: List[Memory]) -> str:
|
40 |
"""
|
41 |
+
Memory 객체 리스트를 받아 LLM을 통해 1~2문장으로 요약
|
42 |
"""
|
43 |
+
if not memories:
|
44 |
+
return ""
|
45 |
+
|
46 |
+
# Memory 객체에서 .content를 추출하여 사용
|
47 |
+
memory_contents = [mem.content for mem in memories]
|
48 |
+
joined = "\n".join([f"- {content}" for content in memory_contents])
|
49 |
+
|
50 |
+
prompt = f"""다음은 한 인물의 과거 기억들입니다:
|
51 |
|
52 |
{joined}
|
53 |
|
54 |
+
→ 이 사람은 어떤 경험을 했는지 1~2문장으로 요약해 주세요."""
|
|
|
55 |
|
56 |
+
# LLM 호출 함수를 사용하여 요약 요청
|
57 |
+
summary = query_llm_with_prompt(prompt)
|
58 |
+
return summary
|
npc_social_network/models/llm_prompt_builder.py
CHANGED
@@ -1,29 +1,62 @@
|
|
1 |
# portfolio/npc_social_network/models/llm_prompt_builder.py
|
2 |
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
"""
|
5 |
-
NPC
|
|
|
|
|
6 |
"""
|
|
|
|
|
|
|
7 |
# 감정 상태 요약
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
|
11 |
-
memory_block = "\n".join([f"- {m}" for m in matched_memories]) if matched_memories else "관련 기억 없음"
|
12 |
|
13 |
# 프롬프트 구성
|
14 |
prompt = f"""
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
-
#
|
22 |
-
{
|
23 |
|
24 |
-
|
25 |
-
{user_input}
|
26 |
|
27 |
-
→
|
|
|
|
|
28 |
"""
|
29 |
-
return prompt
|
|
|
1 |
# portfolio/npc_social_network/models/llm_prompt_builder.py
|
2 |
|
3 |
+
from typing import List, Optional, TYPE_CHECKING
|
4 |
+
from ..npc.npc_memory import Memory
|
5 |
+
|
6 |
+
# 타입 검사 시에만 NPC 클래스를 임포트하여 순환 참조 방지
|
7 |
+
if TYPE_CHECKING:
|
8 |
+
from ..npc.npc_base import NPC
|
9 |
+
from ..npc.npc_memory import Memory
|
10 |
+
|
11 |
+
def build_npc_prompt(npc:"NPC", user_input: str, matched_memories: List[Memory], target_npc: Optional["NPC"]=None) -> str:
|
12 |
"""
|
13 |
+
NPC 대사를 생성하기 위한 강화된 프롬프트 구성 함수
|
14 |
+
- 감정 요약 / 성격 요약 / 관계 / 기억 등 포함
|
15 |
+
- target_npc 인자를 받아 플레이어 또는 다른 NPC와의 대화 모두 처리
|
16 |
"""
|
17 |
+
# 대화 상대방의 이름을 동적으로 설정
|
18 |
+
interlocutor_name = target_npc.name if target_npc else "플레이어"
|
19 |
+
|
20 |
# 감정 상태 요약
|
21 |
+
emotion_summary = npc.summarize_emotional_state()
|
22 |
+
# 성격 요약
|
23 |
+
personality_summary = npc.personality.get_personality_summary()
|
24 |
+
# 관계 요약
|
25 |
+
relationship_description = npc.get_relationship_description(interlocutor_name)
|
26 |
+
|
27 |
+
# 기억 요약 정비
|
28 |
+
memory_summaries = []
|
29 |
+
for mem in matched_memories:
|
30 |
+
# Memory 객체의 content 속성을 사용하도록 수정
|
31 |
+
short_content = mem.content[:50] + "..." if len(mem.content) > 50 else mem.content
|
32 |
+
memory_summaries.append(f"- {short_content} ({mem.emotion})")
|
33 |
|
34 |
+
memory_section = "\n".join(memory_summaries[:3]) if memory_summaries else "없음"
|
|
|
35 |
|
36 |
# 프롬프트 구성
|
37 |
prompt = f"""
|
38 |
+
당신은 '{npc.name}'이라는 이름을 가진 사람입니다.
|
39 |
+
당신의 직업은 '{npc.job}'이고, 현재 '{interlocutor_name}'와(과) 대화를 나누고 있습니다.
|
40 |
+
|
41 |
+
# 당신의 현재 감정 상태:
|
42 |
+
{emotion_summary}
|
43 |
+
|
44 |
+
# 당신의 성격 요약:
|
45 |
+
{personality_summary}
|
46 |
+
|
47 |
+
# 당신과 {interlocutor_name}와(과)의 현재 관계:
|
48 |
+
{relationship_description}
|
49 |
+
|
50 |
+
# 최근 기억:
|
51 |
+
{memory_section}
|
52 |
|
53 |
+
# {interlocutor_name}의 발화
|
54 |
+
"{user_input}"
|
55 |
|
56 |
+
---
|
|
|
57 |
|
58 |
+
→ 이 상황에 어울리는 당신의 자연스럽고 인간적인 사람 반응을 생성하세요.
|
59 |
+
→ 말투, 반응은 감정 상태에 맞추어 주세요.
|
60 |
+
→ 대답은 NPC의 1인칭 시점으로 작성하세요.
|
61 |
"""
|
62 |
+
return prompt
|
npc_social_network/npc/emotion_config.py
CHANGED
@@ -157,19 +157,43 @@ COGNITIVE_RELATION_EMOTIONS = [
|
|
157 |
|
158 |
# 감정별 관계 영향도 정의 (관계에 영향 주는 감정만 등록, 정규화된 [-2.0 ~ +2.0] 범위 안에서 정의)
|
159 |
EMOTION_RELATION_IMPACT = {
|
160 |
-
"joy":
|
161 |
-
"
|
162 |
-
"
|
163 |
-
"
|
164 |
-
"
|
165 |
-
"
|
166 |
-
"
|
167 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
"disgust": -2.0,
|
169 |
-
"fear": -1.
|
170 |
"shame": -1.2,
|
171 |
-
"
|
|
|
|
|
172 |
"regret": -0.9,
|
173 |
-
"
|
174 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
}
|
|
|
157 |
|
158 |
# 감정별 관계 영향도 정의 (관계에 영향 주는 감정만 등록, 정규화된 [-2.0 ~ +2.0] 범위 안에서 정의)
|
159 |
EMOTION_RELATION_IMPACT = {
|
160 |
+
"joy": 1.0,
|
161 |
+
"surprise": 0.2,
|
162 |
+
"gratitude": 1.2,
|
163 |
+
"pride": 0.5,
|
164 |
+
"love": 2.0,
|
165 |
+
"admiration": 0.8,
|
166 |
+
"empathy": 1.0,
|
167 |
+
"hope": 0.9,
|
168 |
+
"anticipatory_joy": 1.2,
|
169 |
+
"calm": 0.7,
|
170 |
+
"engagement": 0.3,
|
171 |
+
"relief": 0.6,
|
172 |
+
"interest": 0.6,
|
173 |
+
|
174 |
+
"sadness": -1.0,
|
175 |
+
"anger": -1.8,
|
176 |
"disgust": -2.0,
|
177 |
+
"fear": -1.2,
|
178 |
"shame": -1.2,
|
179 |
+
"guilt": 0.3,
|
180 |
+
"jealousy": -1.2,
|
181 |
+
"resentment": -1.8,
|
182 |
"regret": -0.9,
|
183 |
+
"schadenfreude": -1.5,
|
184 |
+
"anxiety": -0.5,
|
185 |
+
"confusion": -0.2,
|
186 |
+
"skepticism": -0.2,
|
187 |
+
"boredom": -0.2,
|
188 |
+
|
189 |
+
"compassion": 0.3,
|
190 |
+
"awe": 0.6,
|
191 |
+
"attachment": 1.7,
|
192 |
+
"anticipation": 0.7,
|
193 |
+
"curiosity": 0.6,
|
194 |
+
"nostalgia": 0.2,
|
195 |
+
"bittersweet": 0.1,
|
196 |
+
"rumination": -0.6,
|
197 |
+
"groundedness": 0.5,
|
198 |
+
"comfort": 0.6,
|
199 |
}
|
npc_social_network/npc/npc_base.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1 |
# portfolio/npc_social_network/npc/npc_base.py
|
|
|
2 |
from .npc_memory import Memory, MemoryStore
|
3 |
from .npc_emotion import EmotionManager
|
4 |
from .npc_behavior import BehaviorManager
|
5 |
-
from .emotion_config import EMOTION_CATEGORY_MAP, EMOTION_DECAY_RATE, PERSONALITY_TEMPLATE, EMOTION_RELATION_IMPACT
|
6 |
from .emotion_config import POSITIVE_RELATION_EMOTIONS, NEGATIVE_RELATION_EMOTIONS, COGNITIVE_RELATION_EMOTIONS
|
7 |
from .personality_config import AGE_PROFILE, PERSONALITY_PROFILE
|
8 |
from .npc_relationship import RelationshipManager
|
@@ -13,6 +14,57 @@ from datetime import datetime
|
|
13 |
import random
|
14 |
import copy
|
15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
# NPC 클래스 정의
|
17 |
class NPC:
|
18 |
"""
|
@@ -28,11 +80,11 @@ class NPC:
|
|
28 |
|
29 |
# npc 기억
|
30 |
self.memory_store = MemoryStore()
|
|
|
31 |
# 감정 상태 등 감정 관리자 초기화
|
32 |
-
self.
|
33 |
-
self.personality = copy.deepcopy(personality) if personality else copy.deepcopy(PERSONALITY_TEMPLATE)
|
34 |
self.relationships = RelationshipManager() # 관계 시스템 초기화
|
35 |
-
self.emotion = EmotionManager(
|
36 |
|
37 |
# 행동 관리자
|
38 |
self.behavior = BehaviorManager()
|
@@ -43,8 +95,8 @@ class NPC:
|
|
43 |
for emo in self._emotion_buffer:
|
44 |
base_min = 0.05
|
45 |
base_max = 0.3
|
46 |
-
|
47 |
category = EMOTION_CATEGORY_MAP.get(emo, None)
|
|
|
48 |
|
49 |
if category == "core":
|
50 |
bias = self.personality.get("affect_bias", 1.0)
|
@@ -107,87 +159,80 @@ class NPC:
|
|
107 |
x, y = self.get_position()
|
108 |
screen.blit(self.image, (x * tile_size, y * tile_size))
|
109 |
|
110 |
-
def generate_dialogue(self,user_input=None, use_llm=True):
|
|
|
|
|
|
|
|
|
111 |
"""
|
112 |
-
대화 생성 함수 (기억 검색 기반 프롬프트 포함)
|
113 |
-
"""
|
114 |
if use_llm and user_input:
|
115 |
-
#
|
116 |
-
_, _, matched_memories = search_similar_memories(self
|
117 |
-
|
|
|
118 |
if len(matched_memories) >= 3:
|
119 |
self.summarize_and_store_memories(matched_memories)
|
120 |
|
121 |
-
# 프롬프트 생성
|
122 |
-
prompt = build_npc_prompt(self, user_input, matched_memories)
|
123 |
|
124 |
-
# LLM 호출
|
125 |
npc_reply = query_llm_with_prompt(prompt)
|
126 |
|
127 |
-
#
|
|
|
|
|
|
|
|
|
|
|
128 |
self.remember(
|
129 |
-
content
|
130 |
-
importance
|
131 |
-
emotion
|
132 |
)
|
133 |
|
134 |
-
#
|
135 |
-
|
|
|
|
|
136 |
|
137 |
-
#
|
138 |
-
|
139 |
-
if dominant_emotion:
|
140 |
-
self.update_emotion(dominant_emotion, strength=2.0)
|
141 |
|
142 |
return npc_reply
|
|
|
143 |
else:
|
144 |
-
#
|
145 |
behavior_output, behavior_trace = self.behavior.perform_sequence(
|
146 |
-
self.name, self.job, emotion_buffer=self._emotion_buffer, return_trace
|
147 |
)
|
148 |
|
149 |
-
#
|
150 |
-
|
151 |
-
self.
|
152 |
-
|
153 |
-
|
154 |
-
return_trace=False
|
155 |
-
)
|
156 |
|
157 |
-
#
|
158 |
dominant_emotions = self.behavior.get_layer_dominant_emotions(self._emotion_buffer)
|
159 |
dominant_sequence = self.behavior.decide_layered_sequence(dominant_emotions)
|
|
|
|
|
160 |
|
161 |
-
#
|
162 |
-
if dominant_sequence:
|
163 |
-
dominant_score = max([score for _, score in dominant_sequence])
|
164 |
-
else:
|
165 |
-
# dominant_sequence가 비었으면 기본값 사용 (예: 감정 없음 상태)
|
166 |
-
dominant_score = 5
|
167 |
-
|
168 |
-
# 중요도 제한
|
169 |
-
importance = min(int(dominant_score), 10) # importance 1~10제한
|
170 |
-
|
171 |
-
# Behavior Trace를 Memory에 기록
|
172 |
memory_entry = Memory(
|
173 |
content=f"행동 수행: {behavior_trace}",
|
174 |
importance=importance,
|
175 |
-
emotion
|
176 |
-
behavior_trace
|
177 |
)
|
178 |
self.memory_store.add_memory(memory_entry)
|
179 |
|
180 |
-
#
|
181 |
-
for action, score in behavior_trace:
|
182 |
-
emotion_name = self.behavior.action_to_emotion.get(action)
|
183 |
-
if emotion_name:
|
184 |
-
self.update_emotion(emotion_name, strength=score)
|
185 |
-
|
186 |
-
# Personality 업데이트
|
187 |
self.update_personality()
|
188 |
|
189 |
return str(behavior_output)
|
190 |
-
|
191 |
def remember(self, content: str, importance: int = 5, emotion: str = None, strength: float = 1.0, memory_type:str = "Event"):
|
192 |
"""
|
193 |
NPC가 새로운 기억을 저장하고 감정 상태에 반영
|
@@ -230,20 +275,9 @@ class NPC:
|
|
230 |
"""
|
231 |
감정 상태 요약 텍스트 생성
|
232 |
"""
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
composite = sorted(buffer.items(), key=lambda x: x[1], reverse=True)
|
237 |
-
composite = [(emo, round(score, 2)) for emo, score in composite[:3] if score > 0]
|
238 |
-
composite_str = ", ".join(f"{emo}({val})" for emo, val in composite)
|
239 |
-
return f"감정 평균 강도: {avg_strength} / 대표 감정: {composite_str if composite else '없음' }"
|
240 |
-
|
241 |
-
def interact_with(self, other_npc_name: str, emotion: str, positive: bool):
|
242 |
-
"""
|
243 |
-
다른 NPC와 상호작용 시 감정에 따른 관계 변화를 반영
|
244 |
-
"""
|
245 |
-
delta = self._get_emotion_influence(emotion, positive)
|
246 |
-
self.relationships.update_relationship(other_npc_name, delta)
|
247 |
|
248 |
def _get_emotion_influence(self, emotion: str, positive: bool) -> float:
|
249 |
"""
|
@@ -303,91 +337,6 @@ class NPC:
|
|
303 |
"""
|
304 |
return self.emotion.get_top_emotions(top_n=top_n)
|
305 |
|
306 |
-
def interact_with_player(self, player_input: str) -> str:
|
307 |
-
"""
|
308 |
-
플레이어 입력에 반응 (LLM 기반 응답 생성 + Memory/Emotion/관계 반영)
|
309 |
-
"""
|
310 |
-
# STEP 1: 감정 추론 (선택적)
|
311 |
-
emotion = query_llm_for_emotion(player_input)
|
312 |
-
if emotion:
|
313 |
-
self.update_emotion(emotion, strength=2.0)
|
314 |
-
|
315 |
-
# STEP 2: 관련 기억 검색
|
316 |
-
matched_memories = search_similar_memories(self.name, player_input)
|
317 |
-
|
318 |
-
# STEP 3: 프롬프트 생성
|
319 |
-
prompt = build_npc_prompt(cnp=self,
|
320 |
-
user_input=player_input,
|
321 |
-
matched_memories=matched_memories)
|
322 |
-
|
323 |
-
# STEP 4: LLM 응답 생성
|
324 |
-
npc_reply = query_llm_with_prompt(prompt)
|
325 |
-
|
326 |
-
# STEP 5: 기억 기록
|
327 |
-
memory = Memory(
|
328 |
-
content=f"[플레이어] '{player_input}' → [NPC:{self.name}] '{npc_reply}'",
|
329 |
-
importance=6,
|
330 |
-
emotion=emotion
|
331 |
-
)
|
332 |
-
self.memory_store.add_memory(memory)
|
333 |
-
|
334 |
-
# STEP 6: 감정 기반 관계 변화 반영
|
335 |
-
if isinstance(emotion, dict):
|
336 |
-
delta = 0.0
|
337 |
-
for emo, strength in emotion.items():
|
338 |
-
if emo in EMOTION_RELATION_IMPACT:
|
339 |
-
delta += EMOTION_RELATION_IMPACT[emo] * strength
|
340 |
-
|
341 |
-
if abs(delta) > 0.01: # 영향이 충분히 있을 때만 반영
|
342 |
-
self.relationships.update_relationship("플레이어", delta=delta)
|
343 |
-
print(f"[관계 변화] '{self.name}' → 플레이어: {delta:+.2f}")
|
344 |
-
|
345 |
-
return npc_reply
|
346 |
-
|
347 |
-
def interact_with_npc(self, target_npc):
|
348 |
-
"""
|
349 |
-
다른 NPC와 상호작용 (기본적으로 감정적 메시지 주고 받기 → Memory/Emotion/Relationship 반영)
|
350 |
-
"""
|
351 |
-
# 1. 상호작용 감정 선택 (가장 강한 감정 사용 예시)
|
352 |
-
dominant_emotion = self.emotion.get_dominant_emotion()
|
353 |
-
|
354 |
-
# 방어 코드: dominant_emotion이 None인 경우 → 기본값 대체
|
355 |
-
if dominant_emotion is None:
|
356 |
-
dominant_emotion = "neutral"
|
357 |
-
|
358 |
-
# 2. 메시지 구성 (단순 예시)
|
359 |
-
message = f"{self.name}이(가) {target_npc.name}에게 {dominant_emotion} 감정을 표현했습니다."
|
360 |
-
|
361 |
-
# 3. 상대방 Memory 기록
|
362 |
-
target_npc.remember(
|
363 |
-
content = message,
|
364 |
-
importance = 6,
|
365 |
-
emotion = dominant_emotion,
|
366 |
-
strength = 0.0 # 감정 중복 update 방지
|
367 |
-
)
|
368 |
-
|
369 |
-
# 상대방 Personality 업데이트
|
370 |
-
target_npc.update_personality()
|
371 |
-
|
372 |
-
# 4. 상대방 Emotion 반영
|
373 |
-
target_npc.update_emotion(dominant_emotion, strength=1.5)
|
374 |
-
|
375 |
-
# 5. 상대방 Relationship 반영
|
376 |
-
positive = dominant_emotion in POSITIVE_RELATION_EMOTIONS
|
377 |
-
target_npc.interact_with(self.name, dominant_emotion, positive)
|
378 |
-
|
379 |
-
# 6. 자신도 Memory 기록 (행동 기록)
|
380 |
-
self.remember(
|
381 |
-
content = f"{self.name}이(가) {target_npc.name}에게 감정을 표현함: {dominant_emotion}",
|
382 |
-
importance = 5,
|
383 |
-
emotion = dominant_emotion
|
384 |
-
)
|
385 |
-
|
386 |
-
# 자신 Personality 업데이트
|
387 |
-
self.update_personality()
|
388 |
-
|
389 |
-
return message
|
390 |
-
|
391 |
def update_personality(self):
|
392 |
"""
|
393 |
Memory / Emotion 기반으로 Personality 변화 적용
|
@@ -555,15 +504,51 @@ class NPC:
|
|
555 |
# fallback → 정상화
|
556 |
return 1.0
|
557 |
|
558 |
-
def
|
559 |
"""
|
560 |
기억 리스트를 요약하고, NPC 장기 기억으로 저장
|
561 |
"""
|
562 |
-
if not
|
|
|
|
|
|
|
|
|
|
|
|
|
563 |
return
|
564 |
|
565 |
-
summary = summarize_memories(memory_texts)
|
566 |
self.memory_store.add_memory(
|
567 |
Memory(content=summary, importance=9, emotion=self.emotion.get_dominant_emotion(), memory_type="Summary")
|
568 |
)
|
569 |
-
print(f"[요약 기억 저장됨] {summary}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# portfolio/npc_social_network/npc/npc_base.py
|
2 |
+
from typing import Optional, List
|
3 |
from .npc_memory import Memory, MemoryStore
|
4 |
from .npc_emotion import EmotionManager
|
5 |
from .npc_behavior import BehaviorManager
|
6 |
+
from .emotion_config import EMOTION_LIST, EMOTION_CATEGORY_MAP, EMOTION_DECAY_RATE, PERSONALITY_TEMPLATE, EMOTION_RELATION_IMPACT
|
7 |
from .emotion_config import POSITIVE_RELATION_EMOTIONS, NEGATIVE_RELATION_EMOTIONS, COGNITIVE_RELATION_EMOTIONS
|
8 |
from .personality_config import AGE_PROFILE, PERSONALITY_PROFILE
|
9 |
from .npc_relationship import RelationshipManager
|
|
|
14 |
import random
|
15 |
import copy
|
16 |
|
17 |
+
# Personality 데이터를 관리하고 요약 기능을 제공하는 클래스 정의
|
18 |
+
class PersonalityManager:
|
19 |
+
"""
|
20 |
+
NPC의 성격 데이터를 관리하고 관련 기능을 제공하는 클래스
|
21 |
+
"""
|
22 |
+
def __init__(self, initial_traits=None):
|
23 |
+
self.traits = copy.deepcopy(initial_traits) if initial_traits else copy.deepcopy(PERSONALITY_TEMPLATE)
|
24 |
+
|
25 |
+
def get(self, key, default=None):
|
26 |
+
return self.traits.get(key, default)
|
27 |
+
|
28 |
+
def __getitem__(self, key):
|
29 |
+
return self.traits[key]
|
30 |
+
|
31 |
+
def __setitem__(self, key, value):
|
32 |
+
self.traits[key] = value
|
33 |
+
|
34 |
+
def keys(self):
|
35 |
+
return self.traits.keys()
|
36 |
+
|
37 |
+
def get_personality_summary(self) -> str:
|
38 |
+
"""
|
39 |
+
현재 성격 특성을 바탕으로 자연어 요약을 생성합니다.
|
40 |
+
"""
|
41 |
+
sensitive = self.traits.get("sensitive", 0.5)
|
42 |
+
stoic = self.traits.get("stoic", 0.5)
|
43 |
+
cognitive_bias = self.traits.get("cognitive_bias", 0.5)
|
44 |
+
|
45 |
+
summary_parts = []
|
46 |
+
if sensitive > 0.7:
|
47 |
+
summary_parts.append("매우 민감하고 감정적인 편입니다.")
|
48 |
+
elif sensitive < 0.3:
|
49 |
+
summary_parts.append("상당히 무던하고 감정 표현이 적습니다.")
|
50 |
+
else:
|
51 |
+
summary_parts.append("보통 수준의 감수성을 지녔습니다.")
|
52 |
+
|
53 |
+
if stoic > 0.7:
|
54 |
+
summary_parts.append("역경에 내성이 강하고 감정을 잘 드러내지 않습니다.")
|
55 |
+
elif stoic < 0.3:
|
56 |
+
summary_parts.append("감정의 영향을 쉽게 받는 편입니다.")
|
57 |
+
|
58 |
+
if cognitive_bias > 0.7:
|
59 |
+
summary_parts.append("이성적이고 논리적인 사고를 선호합니다.")
|
60 |
+
elif cognitive_bias < 0.3:
|
61 |
+
summary_parts.append("직관과 감정에 따라 판단하는 경향이 있습니다.")
|
62 |
+
|
63 |
+
if not summary_parts:
|
64 |
+
return "평범하고 균형 잡힌 성격입니다."
|
65 |
+
|
66 |
+
return " ".join(summary_parts)
|
67 |
+
|
68 |
# NPC 클래스 정의
|
69 |
class NPC:
|
70 |
"""
|
|
|
80 |
|
81 |
# npc 기억
|
82 |
self.memory_store = MemoryStore()
|
83 |
+
|
84 |
# 감정 상태 등 감정 관리자 초기화
|
85 |
+
self.personality = PersonalityManager(personality)
|
|
|
86 |
self.relationships = RelationshipManager() # 관계 시스템 초기화
|
87 |
+
self.emotion = EmotionManager(EMOTION_DECAY_RATE, self.personality)
|
88 |
|
89 |
# 행동 관리자
|
90 |
self.behavior = BehaviorManager()
|
|
|
95 |
for emo in self._emotion_buffer:
|
96 |
base_min = 0.05
|
97 |
base_max = 0.3
|
|
|
98 |
category = EMOTION_CATEGORY_MAP.get(emo, None)
|
99 |
+
bias = 1.0
|
100 |
|
101 |
if category == "core":
|
102 |
bias = self.personality.get("affect_bias", 1.0)
|
|
|
159 |
x, y = self.get_position()
|
160 |
screen.blit(self.image, (x * tile_size, y * tile_size))
|
161 |
|
162 |
+
def generate_dialogue(self, user_input: str, target_npc: Optional["NPC"]=None, use_llm: bool=True) -> str:
|
163 |
+
"""
|
164 |
+
플레이어 또는 NPC와 상호작용 시 사용되는 통합 대사 생성 함수
|
165 |
+
- user_input: 입력 문장 (플레이어나 NPC로부터)
|
166 |
+
- target_npc: NPC 간 대화일 경우 상대 NPC
|
167 |
"""
|
|
|
|
|
168 |
if use_llm and user_input:
|
169 |
+
# 1. 유사 기억 검색
|
170 |
+
_, _, matched_memories = search_similar_memories(self, user_input)
|
171 |
+
|
172 |
+
# 2. 기억 요약 및 장기 기억화
|
173 |
if len(matched_memories) >= 3:
|
174 |
self.summarize_and_store_memories(matched_memories)
|
175 |
|
176 |
+
# 3. LLM 프롬프트 생성
|
177 |
+
prompt = build_npc_prompt(self, user_input, matched_memories, target_npc)
|
178 |
|
179 |
+
# 4. LLM 호출
|
180 |
npc_reply = query_llm_with_prompt(prompt)
|
181 |
|
182 |
+
# 5. 감정 추론
|
183 |
+
emotion = query_llm_for_emotion(npc_reply)
|
184 |
+
if emotion and emotion in EMOTION_LIST:
|
185 |
+
self.update_emotion(emotion, strength=2.0)
|
186 |
+
|
187 |
+
# 6. Memory 저장
|
188 |
self.remember(
|
189 |
+
content=f"[입력] '{user_input}' → [응답] '{npc_reply}'",
|
190 |
+
importance=7,
|
191 |
+
emotion=emotion
|
192 |
)
|
193 |
|
194 |
+
# 7. 관계 반영 (플레이어나 다른 NPC)
|
195 |
+
target = target_npc.name if target_npc else "플레이어"
|
196 |
+
if emotion and emotion in EMOTION_RELATION_IMPACT:
|
197 |
+
self.relationships.update_relationship(target, emotion, strength=2.0)
|
198 |
|
199 |
+
# 8. 성격 변화 반영
|
200 |
+
self.update_personality()
|
|
|
|
|
201 |
|
202 |
return npc_reply
|
203 |
+
|
204 |
else:
|
205 |
+
# LLM 미사용 시: 감정 기반 행동 생성
|
206 |
behavior_output, behavior_trace = self.behavior.perform_sequence(
|
207 |
+
self.name, self.job, emotion_buffer=self._emotion_buffer, return_trace=True
|
208 |
)
|
209 |
|
210 |
+
# 행동 기반 감정 추론 및 반영
|
211 |
+
for action, score in behavior_trace:
|
212 |
+
emotion_name = self.behavior.action_to_emotion.get(action)
|
213 |
+
if emotion_name:
|
214 |
+
self.update_emotion(emotion_name, strength=score)
|
|
|
|
|
215 |
|
216 |
+
# dominant_score 기반 중요도 추정
|
217 |
dominant_emotions = self.behavior.get_layer_dominant_emotions(self._emotion_buffer)
|
218 |
dominant_sequence = self.behavior.decide_layered_sequence(dominant_emotions)
|
219 |
+
dominant_score = max([score for _, score in dominant_sequence]) if dominant_sequence else 5
|
220 |
+
importance = min(int(dominant_score), 10)
|
221 |
|
222 |
+
# Memory 저장
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
memory_entry = Memory(
|
224 |
content=f"행동 수행: {behavior_trace}",
|
225 |
importance=importance,
|
226 |
+
emotion=self.emotion.get_dominant_emotion(),
|
227 |
+
behavior_trace=behavior_trace
|
228 |
)
|
229 |
self.memory_store.add_memory(memory_entry)
|
230 |
|
231 |
+
# 성격 업데이트
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
self.update_personality()
|
233 |
|
234 |
return str(behavior_output)
|
235 |
+
|
236 |
def remember(self, content: str, importance: int = 5, emotion: str = None, strength: float = 1.0, memory_type:str = "Event"):
|
237 |
"""
|
238 |
NPC가 새로운 기억을 저장하고 감정 상태에 반영
|
|
|
275 |
"""
|
276 |
감정 상태 요약 텍스트 생성
|
277 |
"""
|
278 |
+
composite = self.get_composite_emotion_state()
|
279 |
+
dominant = self.emotion.get_dominant_emotion()
|
280 |
+
return f"감정 평균 강도: {dominant} / 대표 감정: {composite if composite else '없음' }"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
282 |
def _get_emotion_influence(self, emotion: str, positive: bool) -> float:
|
283 |
"""
|
|
|
337 |
"""
|
338 |
return self.emotion.get_top_emotions(top_n=top_n)
|
339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
340 |
def update_personality(self):
|
341 |
"""
|
342 |
Memory / Emotion 기반으로 Personality 변화 적용
|
|
|
504 |
# fallback → 정상화
|
505 |
return 1.0
|
506 |
|
507 |
+
def summarize_and_store_memories(self, memories: List[Memory]):
|
508 |
"""
|
509 |
기억 리스트를 요약하고, NPC 장기 기억으로 저장
|
510 |
"""
|
511 |
+
if not memories:
|
512 |
+
return
|
513 |
+
|
514 |
+
summary = summarize_memories(memories)
|
515 |
+
|
516 |
+
if "[LLM Error]" in summary:
|
517 |
+
print(f"[요약 실패] LLM 호출 중 에러가 발생하여 요약 기억을 저장하지 않습니다. 에러: {summary}")
|
518 |
return
|
519 |
|
|
|
520 |
self.memory_store.add_memory(
|
521 |
Memory(content=summary, importance=9, emotion=self.emotion.get_dominant_emotion(), memory_type="Summary")
|
522 |
)
|
523 |
+
print(f"[요약 기억 저장됨] {summary}")
|
524 |
+
|
525 |
+
def reflect_memory_emotions_on_relationship(self, target_name: str, memory_limit: int=10):
|
526 |
+
"""
|
527 |
+
최근 기억에 담긴 감정을 기반으로 대상과의 관계를 갱신
|
528 |
+
- 특정 대상 (플레이어 등)에 대해 감정이 담긴 Memory를 수집
|
529 |
+
- 각 감정에 대해 가중치를 곱해 관계 변화량 누적
|
530 |
+
"""
|
531 |
+
recent_memories = self.memory_store.get_recent_memories(limit=memory_limit)
|
532 |
+
emotion_accumulator = {}
|
533 |
+
|
534 |
+
for mem in recent_memories:
|
535 |
+
if target_name not in mem.content:
|
536 |
+
continue # 대상이 언급된 기억만 추출
|
537 |
+
|
538 |
+
if isinstance(mem.emotion, dict):
|
539 |
+
for emo, value in mem.emotion.items():
|
540 |
+
if emo not in EMOTION_RELATION_IMPACT:
|
541 |
+
continue
|
542 |
+
emotion_accumulator[emo] = emotion_accumulator.get(emo, 0.0) + value
|
543 |
+
|
544 |
+
# 전체 감정 누적값을 바탕으로 관계 변화량 계선
|
545 |
+
total_delta = 0.0
|
546 |
+
for emo, value in emotion_accumulator.items():
|
547 |
+
for emo, value in emotion_accumulator.items():
|
548 |
+
weight = EMOTION_RELATION_IMPACT[emo]
|
549 |
+
delta = weight * value
|
550 |
+
total_delta += delta
|
551 |
+
|
552 |
+
if abs(total_delta) > 0.01:
|
553 |
+
self.relationships.update_relationship(target_name, delta=total_delta)
|
554 |
+
print(f"[기억 기반 관계 변화] {self.name} ↔ {target_name}: {total_delta:.2f}")
|
npc_social_network/npc/npc_memory_embedder.py
CHANGED
@@ -4,9 +4,10 @@ from sentence_transformers import SentenceTransformer
|
|
4 |
import numpy as np
|
5 |
import faiss
|
6 |
import os
|
7 |
-
from .npc_memory import MemoryStore
|
8 |
-
from .npc_base import NPC
|
9 |
import re
|
|
|
|
|
|
|
10 |
|
11 |
# 사전 훈련된 문장 임베딩 모델
|
12 |
model = SentenceTransformer("all-MiniLM-L6-v2")
|
@@ -36,7 +37,7 @@ def embed_memory(memory):
|
|
36 |
embedding = model.encode(text)
|
37 |
return embedding
|
38 |
|
39 |
-
def embed_npc_memories(npc: NPC):
|
40 |
"""
|
41 |
특정 NPC의 기억 전체를 임베딩하고 FAISS index로 저장
|
42 |
"""
|
@@ -68,28 +69,21 @@ def load_npc_faiss_index(npc_name):
|
|
68 |
index = faiss.read_index(index_path)
|
69 |
return index
|
70 |
|
71 |
-
def search_similar_memories(
|
72 |
"""
|
73 |
질의 문장을 벡터로 변환하고 FAISS에서 유사한 기억 검색
|
74 |
"""
|
75 |
-
|
76 |
-
|
77 |
-
index = load_npc_faiss_index(npc_name)
|
78 |
query_vec = model.encode([query])
|
79 |
distances, indices = index.search(np.array(query_vec, dtype=np.float32), top_k)
|
80 |
|
81 |
-
# 보다 정확한 디버깅을 위해 추가 (나중에 제거 가능)
|
82 |
-
npc = npc_manager.get_npc_by_name(npc_name)
|
83 |
all_memories = npc.memory_store.get_all_memories()
|
84 |
|
85 |
-
|
|
|
|
|
86 |
|
87 |
-
|
88 |
-
print(f"[{npc_name}] '{query}'에 대한 유사 기억 검색 결과:")
|
89 |
-
for rank, (idx, dist) in enumerate(zip(indices[0], distances[0])):
|
90 |
-
print(f" {rank+1}. index: {idx}, distance: {dist:.4f} → {all_memories[idx].content}")
|
91 |
-
|
92 |
-
return indices[0], distances[0], matched_texts
|
93 |
|
94 |
def sanitize_filename(name):
|
95 |
"""
|
|
|
4 |
import numpy as np
|
5 |
import faiss
|
6 |
import os
|
|
|
|
|
7 |
import re
|
8 |
+
from typing import TYPE_CHECKING
|
9 |
+
if TYPE_CHECKING:
|
10 |
+
from .npc_base import NPC
|
11 |
|
12 |
# 사전 훈련된 문장 임베딩 모델
|
13 |
model = SentenceTransformer("all-MiniLM-L6-v2")
|
|
|
37 |
embedding = model.encode(text)
|
38 |
return embedding
|
39 |
|
40 |
+
def embed_npc_memories(npc: "NPC"):
|
41 |
"""
|
42 |
특정 NPC의 기억 전체를 임베딩하고 FAISS index로 저장
|
43 |
"""
|
|
|
69 |
index = faiss.read_index(index_path)
|
70 |
return index
|
71 |
|
72 |
+
def search_similar_memories(npc: "NPC", query: str, top_k=3):
|
73 |
"""
|
74 |
질의 문장을 벡터로 변환하고 FAISS에서 유사한 기억 검색
|
75 |
"""
|
76 |
+
index = load_npc_faiss_index(npc.name)
|
|
|
|
|
77 |
query_vec = model.encode([query])
|
78 |
distances, indices = index.search(np.array(query_vec, dtype=np.float32), top_k)
|
79 |
|
|
|
|
|
80 |
all_memories = npc.memory_store.get_all_memories()
|
81 |
|
82 |
+
# 검색된 인덱스가 메모리 범위를 벗어나지 않는지 확인
|
83 |
+
valid_indices = [i for i in indices[0] if i < len(all_memories)]
|
84 |
+
matched_memories = [all_memories[i] for i in valid_indices]
|
85 |
|
86 |
+
return valid_indices, distances[0][:len(valid_indices)], matched_memories
|
|
|
|
|
|
|
|
|
|
|
87 |
|
88 |
def sanitize_filename(name):
|
89 |
"""
|
npc_social_network/npc/npc_relationship.py
CHANGED
@@ -6,12 +6,26 @@ class RelationshipManager:
|
|
6 |
# 관계 사전: {상대방 이름: 점수 (float)}
|
7 |
self.relationships = {}
|
8 |
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
# 상대 NPC와의 현재 관계 점수를 반환
|
17 |
def get_relationship(self, other_npc_name:str) -> float:
|
|
|
6 |
# 관계 사전: {상대방 이름: 점수 (float)}
|
7 |
self.relationships = {}
|
8 |
|
9 |
+
def update_relationship(self, other: str, emotion: str, strength: float=1.0):
|
10 |
+
"""
|
11 |
+
특정 감정 기반으로 관계 수치 조정
|
12 |
+
- 긍정/부정만이 아니라 감정 유형에 따른 영향 차별화
|
13 |
+
"""
|
14 |
+
from .emotion_config import EMOTION_RELATION_IMPACT
|
15 |
+
|
16 |
+
# 기본 로직
|
17 |
+
prev = self.relationships.get(other, 0.0)
|
18 |
+
|
19 |
+
impact = EMOTION_RELATION_IMPACT.get(emotion, 0.0)
|
20 |
+
new_value = prev + (impact * strength)
|
21 |
+
|
22 |
+
# 감쇠 적용 (값이 커질수록 변화폭을 줄여 급격한 변화 방지)
|
23 |
+
if abs(new_value) > abs(prev):
|
24 |
+
new_value = prev + (impact * strength * 0.7) # 감쇠 비율 예시
|
25 |
+
|
26 |
+
# 클리핑 (-100 ~ 100 범위 제한)
|
27 |
+
new_value = max(min(new_value, 100), -100)
|
28 |
+
self.relationships[other] = round(new_value, 2)
|
29 |
|
30 |
# 상대 NPC와의 현재 관계 점수를 반환
|
31 |
def get_relationship(self, other_npc_name:str) -> float:
|
npc_social_network/routes/npc_route.py
CHANGED
@@ -63,12 +63,15 @@ def chat():
|
|
63 |
if npc is None:
|
64 |
return jsonify({"error": "NPC not found"}), 404
|
65 |
|
66 |
-
#
|
67 |
-
npc_reply
|
|
|
|
|
|
|
68 |
|
69 |
return jsonify({
|
70 |
"npc_reply": npc_reply,
|
71 |
-
"dominant_emotion":
|
72 |
"memory_summary": npc.summarize_emotional_state(),
|
73 |
"emotion_state": npc.get_composite_emotion_state(),
|
74 |
"relationship_with_player": npc.get_relationship_description("플레이어")
|
|
|
63 |
if npc is None:
|
64 |
return jsonify({"error": "NPC not found"}), 404
|
65 |
|
66 |
+
# generate_dialogue를 통해 응답 생성 및 내부 상태 업데이트
|
67 |
+
npc_reply = npc.generate_dialogue(user_input, use_llm=True)
|
68 |
+
|
69 |
+
# 업데이트된 상태를 별도로 조회
|
70 |
+
dominant_emotion = npc.emotion.get_dominant_emotion()
|
71 |
|
72 |
return jsonify({
|
73 |
"npc_reply": npc_reply,
|
74 |
+
"dominant_emotion": dominant_emotion,
|
75 |
"memory_summary": npc.summarize_emotional_state(),
|
76 |
"emotion_state": npc.get_composite_emotion_state(),
|
77 |
"relationship_with_player": npc.get_relationship_description("플레이어")
|
run_npc_interactions.py
CHANGED
@@ -1,14 +1,39 @@
|
|
1 |
# portfolio/run_npc_interactions.py
|
2 |
|
3 |
import time
|
4 |
-
from npc_social_network.
|
|
|
5 |
|
6 |
-
def
|
7 |
-
|
8 |
-
print(f"[Background] NPC interactions triggered.", flush=True)
|
9 |
-
npc_manager.npc_interactions()
|
10 |
-
time.sleep(interval_seconds)
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# portfolio/run_npc_interactions.py
|
2 |
|
3 |
import time
|
4 |
+
from npc_social_network.npc.npc_manager import npc_manager
|
5 |
+
from npc_social_network.npc.npc_base import NPC
|
6 |
|
7 |
+
def run_npc_test_scenario(npc_name="엘라"):
|
8 |
+
npc = npc_manager.get_npc_by_name[npc_name]
|
|
|
|
|
|
|
9 |
|
10 |
+
test_inputs = [
|
11 |
+
"오늘 하루는 최악이었어. 왜 다들 날 무시하는 거지?",
|
12 |
+
"고마워. 나 도와줘서 정말 감사했어.",
|
13 |
+
"그때 화낸 건 미안해. 내 잘못이었어.",
|
14 |
+
"내가 선물한 책 어땠어?",
|
15 |
+
"우리 예전에 함께 일했던 거 기억나?",
|
16 |
+
]
|
17 |
+
|
18 |
+
for input_text in test_inputs:
|
19 |
+
print(f"\n[플레이어 입력] {input_text}")
|
20 |
+
response = npc.interact_with_player(input_text)
|
21 |
+
print(f"[NPC 응답] {response}")
|
22 |
+
|
23 |
+
# 기억 기반 관계 반영까지 테스트
|
24 |
+
npc.reflect_memory_emotions_on_relationship("플레이어")
|
25 |
+
|
26 |
+
# 상태 요약 출력
|
27 |
+
print("\n🧠 현재 감정 상태 요약:")
|
28 |
+
print(npc.summarize_emotional_state())
|
29 |
+
|
30 |
+
print("\n📚 최근 기억:")
|
31 |
+
for m in npc.memory_store.get_recent_memories(limit=5):
|
32 |
+
print(f"- {m.content} ({m.emotion})")
|
33 |
+
|
34 |
+
print("\n🔗 플레이어와의 관계:")
|
35 |
+
print(npc.get_relationship_description("플레이어"))
|
36 |
+
|
37 |
+
|
38 |
+
if __name__ == "__main__":
|
39 |
+
run_npc_test_scenario()
|
test.ipynb
CHANGED
@@ -3133,6 +3133,139 @@
|
|
3133 |
"# 복합 감정 기반 행동 시퀀스 출력\n",
|
3134 |
"print(npc.generate_dialogue())"
|
3135 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3136 |
}
|
3137 |
],
|
3138 |
"metadata": {
|
|
|
3133 |
"# 복합 감정 기반 행동 시퀀스 출력\n",
|
3134 |
"print(npc.generate_dialogue())"
|
3135 |
]
|
3136 |
+
},
|
3137 |
+
{
|
3138 |
+
"cell_type": "code",
|
3139 |
+
"execution_count": 1,
|
3140 |
+
"id": "246a140b",
|
3141 |
+
"metadata": {},
|
3142 |
+
"outputs": [
|
3143 |
+
{
|
3144 |
+
"name": "stderr",
|
3145 |
+
"output_type": "stream",
|
3146 |
+
"text": [
|
3147 |
+
"c:\\Users\\human\\.conda\\envs\\portfolio\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
3148 |
+
" from .autonotebook import tqdm as notebook_tqdm\n"
|
3149 |
+
]
|
3150 |
+
},
|
3151 |
+
{
|
3152 |
+
"name": "stdout",
|
3153 |
+
"output_type": "stream",
|
3154 |
+
"text": [
|
3155 |
+
"\n",
|
3156 |
+
"[플레이어 입력] 오늘 하루는 최악이었어. 왜 다들 날 무시하는 거지?\n",
|
3157 |
+
"[요약 기억 저장됨] 이 사람은 최근 찰리와 함께 농사일을 돕고, 밥에게 선물 받은 검을 마음에 들어 하는 긍정적이거나 일상적인 경험과 더불어 앨리스와 심하게 다투는 갈등 상황도 겪었습니다. 즉, 다양한 사람들과 여러 종류의 상호작용과 활동을 경험한 것으로 보입니다.\n",
|
3158 |
+
"[Personality Update] 엘라 → sensitive: 1.00, stoic: 0.00, cognitive_bias: 1.00\n",
|
3159 |
+
"[NPC 응답] 으음... 오늘 하루가 최악이었다니... 정말 힘들었겠네.\n",
|
3160 |
+
"\n",
|
3161 |
+
"'다들' 이라니... 정확히 누가, 왜 그랬다는 거야? 무시당하는 기분은... 나도 알 것 같기도 하고...\n",
|
3162 |
+
"\n",
|
3163 |
+
"[플레이어 입력] 고마워. 나 도와줘서 정말 감사했어.\n",
|
3164 |
+
"[요약 실패] LLM 호출 중 에러가 발생하여 요약 기억을 저장하지 않습니다. 에러: [LLM Error] 500 An internal error has occurred. Please retry or report in https://developers.generativeai.google/guide/troubleshooting\n",
|
3165 |
+
"[Personality Update] 엘라 → sensitive: 1.00, stoic: 0.00, cognitive_bias: 1.00\n",
|
3166 |
+
"[NPC 응답] 아냐, 괜찮아. 별거 아니었는데 뭘. 네가 그렇게 말해주니 오히려 내가 다 고맙네.\n",
|
3167 |
+
"\n",
|
3168 |
+
"[플레이어 입력] 그때 화낸 건 미안해. 내 잘못이었어.\n",
|
3169 |
+
"[요약 기억 저장됨] 이 인물은 최근 찰리와 농사일을 하고 밥에게 선물을 받는 등 긍정적인 경험과 더불어, 앨리스와 심하게 다투고 타인에게 무시당한다는 느낌을 받는 등 부정적인 갈등 상황도 겪었습니다. 즉, 다양한 사람들과 교류하며 좋은 경험과 나쁜 경험을 모두 경험했습니다.\n",
|
3170 |
+
"[Personality Update] 엘라 → sensitive: 1.00, stoic: 0.00, cognitive_bias: 1.00\n",
|
3171 |
+
"[NPC 응답] 아... 그때 화냈던 거 말이지...?\n",
|
3172 |
+
"\n",
|
3173 |
+
"음... 괜찮아. 누구나 가끔은 감정에 휩쓸릴 때가 있는 거니까. 나도 그런 걸.\n",
|
3174 |
+
"\n",
|
3175 |
+
"그렇게 말해줘서 고마워.\n",
|
3176 |
+
"\n",
|
3177 |
+
"[플레이어 입력] 내가 선물한 책 어땠어?\n",
|
3178 |
+
"[요약 실패] LLM 호출 중 에러가 발생하여 요약 기억을 저장하지 않습니다. 에러: [LLM Error] 500 An internal error has occurred. Please retry or report in https://developers.generativeai.google/guide/troubleshooting\n",
|
3179 |
+
"[Personality Update] 엘라 → sensitive: 1.00, stoic: 0.00, cognitive_bias: 1.00\n",
|
3180 |
+
"[NPC 응답] 아... 네가 선물해 준 그 책 말이지...?\n",
|
3181 |
+
"\n",
|
3182 |
+
"(잠깐 생각에 잠긴 듯)\n",
|
3183 |
+
"\n",
|
3184 |
+
"음... 꽤... 아니, 아주 흥미로운 내용이었어. 특히 그... (책 내용 중 특정 부분을 떠올리려는 듯) 아, 맞아. 그 이론에 대해 설명하는 부분 말이야. 논리적으로 잘 정리되어 있어서 이해하기 쉬웠어. 내... 내 마법 연구에도 조금 도움이 될 것 같다는 생각도 들었고.\n",
|
3185 |
+
"\n",
|
3186 |
+
"...사실 요즘 이것저것 생각할 게 많아서 머리가 복잡했는데, 네가 준 책을 읽는 동안에는 다른 생각들을 다 잊고 내용에만 집중할 수 있었어.\n",
|
3187 |
+
"\n",
|
3188 |
+
"정말 좋은 선물이었어. 고마워.\n",
|
3189 |
+
"\n",
|
3190 |
+
"[플레이어 입력] 우리 예전에 함께 일했던 거 기억나?\n",
|
3191 |
+
"[요약 실패] LLM 호출 중 에러가 발생하여 요약 기억을 저장하지 않습니다. 에러: [LLM Error] 500 An internal error has occurred. Please retry or report in https://developers.generativeai.google/guide/troubleshooting\n",
|
3192 |
+
"[Personality Update] 엘라 → sensitive: 1.00, stoic: 0.00, cognitive_bias: 1.00\n",
|
3193 |
+
"[NPC 응답] ```korean\n",
|
3194 |
+
"엘라: 응... 우리 함께 일했던 거? 기억나지, 물론.\n",
|
3195 |
+
"\n",
|
3196 |
+
"엘라: 음... 어떤 때를 말하는 거야? 우리 함께 했던 일들은... 대부분 좋은 기억으로 남아있으니까.\n",
|
3197 |
+
"```\n",
|
3198 |
+
"\n",
|
3199 |
+
"🧠 현재 감정 상태 요약:\n",
|
3200 |
+
"감정 평균 강도: nostalgia / 대표 감정: [('nostalgia', 3.0)]\n",
|
3201 |
+
"\n",
|
3202 |
+
"📚 최근 기억:\n",
|
3203 |
+
"- [입력] '우리 예전�� 함께 일했던 거 기억나?' → [응답] '```korean\n",
|
3204 |
+
"엘라: 응... 우리 함께 일했던 거? 기억나지, 물론.\n",
|
3205 |
+
"\n",
|
3206 |
+
"엘라: 음... 어떤 때를 말하는 거야? 우리 함께 했던 일들은... 대부분 좋은 기억으로 남아있으니까.\n",
|
3207 |
+
"```' (nostalgia)\n",
|
3208 |
+
"- [입력] '내가 선물한 책 어땠어?' → [응답] '아... 네가 선물해 준 그 책 말이지...?\n",
|
3209 |
+
"\n",
|
3210 |
+
"(잠깐 생각에 잠긴 듯)\n",
|
3211 |
+
"\n",
|
3212 |
+
"음... 꽤... 아니, 아주 흥미로운 내용이었어. 특히 그... (책 내용 중 특정 부분을 떠올리려는 듯) 아, 맞아. 그 이론에 대해 설명하는 부분 말이야. 논리적으로 잘 정리되어 있어서 이해하기 쉬웠어. 내... 내 마법 연구에도 조금 도움이 될 것 같다는 생각도 들었고.\n",
|
3213 |
+
"\n",
|
3214 |
+
"...사실 요즘 이것저것 생각할 게 많아서 머리가 복잡했는데, 네가 준 책을 읽는 동안에는 다른 생각들을 다 잊고 내용에만 집중할 수 있었어.\n",
|
3215 |
+
"\n",
|
3216 |
+
"정말 좋은 선물이었어. 고마워.' (gratitude)\n",
|
3217 |
+
"- [입력] '그때 화낸 건 미안해. 내 잘못이었어.' → [응답] '아... 그때 화냈던 거 말이지...?\n",
|
3218 |
+
"\n",
|
3219 |
+
"음... 괜찮아. 누구나 가끔은 감정에 휩쓸릴 때가 있는 거니까. 나도 그런 걸.\n",
|
3220 |
+
"\n",
|
3221 |
+
"그렇게 말해줘서 고마워.' (gratitude)\n",
|
3222 |
+
"- 이 인물은 최근 찰리와 농사일을 하고 밥에게 선물을 받는 등 긍정적인 경험과 더불어, 앨리스와 심하게 다투고 타인에게 무시당한다는 느낌을 받는 등 부정적인 갈등 상황도 겪었습니다. 즉, 다양한 사람들과 교류하며 좋은 경험과 나쁜 경험을 모두 경험했습니다. (None)\n",
|
3223 |
+
"- [입력] '고마워. 나 도와줘서 정말 감사했어.' → [응답] '아냐, 괜찮아. 별거 아니었는데 뭘. 네가 그렇게 말해주니 오히려 내가 다 고맙네.' (gratitude)\n",
|
3224 |
+
"\n",
|
3225 |
+
"🔗 플레이어와의 관계:\n",
|
3226 |
+
"호감 있음\n"
|
3227 |
+
]
|
3228 |
+
}
|
3229 |
+
],
|
3230 |
+
"source": [
|
3231 |
+
"# portfolio/test.ipynb\n",
|
3232 |
+
"\n",
|
3233 |
+
"import time\n",
|
3234 |
+
"from npc_social_network.routes.npc_route import npc_manager\n",
|
3235 |
+
"\n",
|
3236 |
+
"def run_npc_test_scenario(npc_name=\"엘라\"):\n",
|
3237 |
+
" npc = npc_manager.get_npc_by_name(npc_name)\n",
|
3238 |
+
"\n",
|
3239 |
+
" test_inputs = [\n",
|
3240 |
+
" \"오늘 하루는 최악이었어. 왜 다들 날 무시하는 거지?\",\n",
|
3241 |
+
" \"고마워. 나 도와줘서 정말 감사했어.\",\n",
|
3242 |
+
" \"그때 화낸 건 미안해. 내 잘못이었어.\",\n",
|
3243 |
+
" \"내가 선물한 책 어땠어?\",\n",
|
3244 |
+
" \"우리 예전에 함께 일했던 거 기억나?\",\n",
|
3245 |
+
" ]\n",
|
3246 |
+
"\n",
|
3247 |
+
" for input_text in test_inputs:\n",
|
3248 |
+
" print(f\"\\n[플레이어 입력] {input_text}\")\n",
|
3249 |
+
" response = npc.generate_dialogue(input_text)\n",
|
3250 |
+
" print(f\"[NPC 응답] {response}\")\n",
|
3251 |
+
"\n",
|
3252 |
+
" # 기억 기반 관계 반영까지 테스트\n",
|
3253 |
+
" npc.reflect_memory_emotions_on_relationship(\"플레이어\")\n",
|
3254 |
+
"\n",
|
3255 |
+
" # 상태 요약 출력\n",
|
3256 |
+
" print(\"\\n🧠 현재 감정 상태 요약:\")\n",
|
3257 |
+
" print(npc.summarize_emotional_state())\n",
|
3258 |
+
"\n",
|
3259 |
+
" print(\"\\n📚 최근 기억:\")\n",
|
3260 |
+
" for m in npc.memory_store.get_recent_memories(limit=5):\n",
|
3261 |
+
" print(f\"- {m.content} ({m.emotion})\")\n",
|
3262 |
+
"\n",
|
3263 |
+
" print(\"\\n🔗 플레이어와의 관계:\")\n",
|
3264 |
+
" print(npc.get_relationship_description(\"플레이어\"))\n",
|
3265 |
+
"\n",
|
3266 |
+
"\n",
|
3267 |
+
"run_npc_test_scenario()"
|
3268 |
+
]
|
3269 |
}
|
3270 |
],
|
3271 |
"metadata": {
|