Spaces:
Sleeping
Sleeping
# npc_social_network/models/llm_helper.py | |
from npc_social_network.npc.emotion_config import EMOTION_LIST | |
from npc_social_network.npc.npc_memory import Memory | |
from .. import simulation_core | |
from typing import List | |
import json | |
import re | |
import time | |
from google.api_core import exceptions as google_exceptions | |
from typing import TYPE_CHECKING, List | |
if TYPE_CHECKING: | |
from ..npc.npc_base import NPC | |
from ..manager.conversation_manager import Conversation | |
def query_llm_for_emotion(user_input): | |
""" | |
LLM์ ํตํด ํ๋ ์ด์ด ์ ๋ ฅ์์ ๊ฐ์ ์ถ์ถ | |
""" | |
prompt = f""" | |
ํ๋ ์ด์ด ์ ๋ ฅ: "{user_input}" | |
์๋ ๊ฐ์ ๋ฆฌ์คํธ ์ค ๊ฐ์ฅ ์ ํด๋น๋๋ ๊ฐ์ ์ ํ ๊ฐ๋ง ๊ณจ๋ผ ๋ฐํํ์ธ์. | |
๊ฐ์ ๋ฆฌ์คํธ: {', '.join(EMOTION_LIST)} | |
๋ฐ๋์ ๊ฐ์ ์ด๋ฆ์ ํ ๊ฐ๋ง ์ถ๋ ฅํ์ธ์. (์: joy) | |
""" | |
try: | |
response = simulation_core.active_llm_model.generate_content(prompt) | |
return response.text.strip() | |
except Exception as e: | |
return f"[LLM Error] {str(e)}" | |
def query_llm_with_prompt(prompt: str) -> str: | |
""" | |
prompt ๋ฌธ์์ด์ ๋ฐ์ LLM ํธ์ถ | |
- api ์ฌ์ฉ๋ ์ค๋ฅ๋ฅผ ๊ฐ์งํ๊ณ ์๋ ๋์ฒ | |
""" | |
if not simulation_core.active_llm_model: | |
return "[LLM Error] API ๋ชจ๋ธ์ด ์ ์์ ์ผ๋ก ์ค์ ๋์ง ์์์ต๋๋ค." | |
try: | |
# 1. ์ฒซ ๋ฒ์งธ API ํธ์ถ ์๋ | |
response = simulation_core.active_llm_model.generate_content(prompt) | |
return response.text.strip() | |
except google_exceptions.ResourceExhausted as e: | |
error_message = str(e) | |
# 2. ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋ถ์ํ์ฌ ์์ธ ํ์ | |
if "PerMinute" in error_message: | |
# -- ๋ถ๋น ์ฌ์ฉ๋ ์ด๊ณผ -- | |
simulation_core.add_log("[API ๊ฒฝ๊ณ ] ๋ถ๋น ์ฌ์ฉ๋์ ์ด๊ณผํ์ต๋๋ค. 60์ด ํ ์๋์ผ๋ก ์ฌ์๋ ํฉ๋๋ค.") | |
time.sleep(60) | |
try: | |
# 2-1. ๋ ๋ฒ์งธ API ํธ์ถ ์๋ | |
simulation_core.add_log("[API ์ ๋ณด] API ํธ์ถ์ ์ฌ์๋ํฉ๋๋ค.") | |
response = simulation_core.active_llm_model.generate_content(prompt) | |
return response.text.strip() | |
except Exception as retry_e: | |
# ์ฌ์๋๋ง์ ์คํจํ ๊ฒฝ์ฐ, ์ต์ข ์๋ฌ ๋ฐํ | |
simulation_core.add_log("========= [API Error] =========") | |
# ์๋ฌผ๋ ์ด์ ์ ์์ ํ๊ฒ '์ผ์์ ์ง' ์ํ๋ก ๋ณ๊ฒฝ | |
with simulation_core.simulation_lock: | |
simulation_core.simulation_paused = True | |
return f"[API ์๋ฌ] ์ฌ์๋์ ์คํจํ์ต๋๋ค: {retry_e}" | |
elif "PerDay" in error_message: | |
# -- ์ผ์ผ ์ฌ์ฉ๋ ์ด๊ณผ ์ -- | |
simulation_core.add_log("[API Error] ์ผ์ผ ์ฌ์ฉ๋์ ๋ชจ๋ ์์งํ์ต๋๋ค.") | |
simulation_core.add_log("๋ค๋ฅธ API ํค๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ๋ด์ผ ๋ค์ ์๋ํด์ฃผ์ธ์.") | |
# ์๋ฎฌ๋ ์ด์ ์ ์์ ํ๊ฒ '์ผ์์ ์ง' ์ํ๋ก ๋ณ๊ฒฝ | |
with simulation_core.simulation_lock: | |
simulation_core.simulation_paused = True | |
return "[LLM Error] Daily quota exceeded" | |
else: | |
# ๊ทธ ์ธ ๋ค๋ฅธ ResourceExhausted ์ค๋ฅ | |
with simulation_core.simulation_lock: | |
simulation_core.simulation_paused = True | |
return f"[LLM Error] {error_message}" | |
except Exception as e: | |
# ๊ทธ ์ธ ๋ชจ๋ ์ข ๋ฅ์ ์ค๋ฅ | |
return f"[LLM Error] {str(e)}" | |
def summarize_text(text: str) -> str: | |
"""์ฃผ์ด์ง ๊ธด ํ ์คํธ(๋ํ ๋ด์ฉ ๋ฑ)๋ฅผ ํ ๋ฌธ์ฅ์ผ๋ก ์์ฝ""" | |
if not text.strip(): | |
return "์๋ฌด ๋ด์ฉ ์๋ ๋ํ์์" | |
prompt = f""" | |
# Instruction | |
๋ค์ ํ ์คํธ๋ฅผ ๊ฐ์ฅ ํต์ฌ์ ์ธ ๋ด์ฉ์ ๋ด์ ํ๊ตญ์ด ํ ๋ฌธ์ฅ์ผ๋ก ์์ฝํด์ฃผ์ธ์. | |
# Text | |
{text} | |
# Summary (a single sentence): | |
""" | |
summary = query_llm_with_prompt(prompt) | |
return summary if summary and "[LLM Error]" not in summary else "๋ํ ์์ฝ์ ์คํจํจ" | |
def summarize_memories(memories: List[Memory]) -> str: | |
""" | |
Memory ๊ฐ์ฒด ๋ฆฌ์คํธ๋ฅผ ๋ฐ์ LLM์ ํตํด 1~2๋ฌธ์ฅ์ผ๋ก ์์ฝ | |
""" | |
if not memories: | |
return "" | |
# Memory ๊ฐ์ฒด์์ .content๋ฅผ ์ถ์ถํ์ฌ ์ฌ์ฉ | |
memory_contents = [mem.content for mem in memories] | |
joined = "\n".join([f"- {content}" for content in memory_contents]) | |
prompt = f"""๋ค์์ ํ ์ธ๋ฌผ์ ๊ณผ๊ฑฐ ๊ธฐ์ต๋ค์ ๋๋ค: | |
{joined} | |
โ ์ด ์ฌ๋์ ์ด๋ค ๊ฒฝํ์ ํ๋์ง 1~2๋ฌธ์ฅ์ผ๋ก ์์ฝํด ์ฃผ์ธ์.""" | |
# LLM ํธ์ถ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ์์ฝ ์์ฒญ | |
summary = query_llm_with_prompt(prompt) | |
return summary | |
def analyze_gossip(gossip_content: str) -> dict: | |
"""LLM์ ์ด์ฉํด ์๋ฌธ์ ๋ถ์ํ๊ณ , ๊ด๋ จ ์ธ๋ฌผ๊ณผ ๊ธ/๋ถ์ ์ JSON์ผ๋ก ๋ฐํํฉ๋๋ค.""" | |
prompt=""" | |
# ์ง์์ฌํญ | |
๋ค์ ์๋ฌธ ๋ด์ฉ์์ ํต์ฌ ์ธ๋ฌผ 2๋ช ๊ณผ, ๊ทธ๋ค ์ฌ์ด์ ์ํธ์์ฉ์ด ๊ธ์ ์ ์ธ์ง, ๋ถ์ ์ ์ธ์ง, ์ค๋ฆฝ์ ์ธ์ง ๋ถ์ํด์ค. | |
๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋์ ์๋ JSON ํ์์ผ๋ก๋ง ์๋ตํด์ค, ๋ค๋ฅธ ์ค๋ช ์ ๋ถ์ด์ง ๋ง. | |
# ์๋ฌธ ๋ด์ฉ | |
"{gossip_content}" | |
# ๋ถ์ ๊ฒฐ๊ณผ (JSON ํ์) | |
{{ | |
"person1": "์ฒซ ๋ฒ์งธ ์ธ๋ฌผ ์ด๋ฆ", | |
"person2": "๋ ๋ฒ์งธ ์ธ๋ฌผ ์ด๋ฆ", | |
"sentiment": "positive or negative or neutral", | |
"reason": "ํ ๋ฌธ์ฅ์ผ๋ก ์์ฝํ ์ด์ " | |
}} | |
""" | |
try: | |
response_text = query_llm_with_prompt(prompt) | |
# LLM ์๋ต์์ JSON ๋ถ๋ถ๋ง ์ถ์ถ | |
json_match = re.search(r'\{.*\}', response_text, re.DOTALL) | |
if json_match: | |
return json.loads(json_match.group()) | |
return {} | |
except (json.JSONDecodeError, Exception) as e: | |
print(f"์๋ฌธ ๋ถ์ ์ค ์ค๋ฅ ๋ฐ์: {e}") | |
return {} | |
def classify_and_extract_plan_details(plan_step: str) -> dict: | |
""" | |
๊ณํ ๋จ๊ณ๋ฅผ ๋ถ์ํ์ฌ ํ๋ ํ์ , ๋์, ์ฃผ์ ๋ฅผ ์ถ์ถ. | |
""" | |
prompt = f""" | |
# Instruction | |
Analyze the following action plan. Classify its type and extract the target NPC and topic if applicable. | |
Your response MUST be in JSON format. | |
Action Types can be "TALK" (for conversing with others), "HELP" (for helping someone), or "SOLO_ACTION" (for acting alone). | |
# Action Plan | |
"{plan_step}" | |
# Analysis (JSON format only) | |
{{ | |
"action_type": "TALK or SOLO_ACTION", | |
"target": "NPC's name or null", | |
"topic": "topic of conversation or null" | |
}} | |
""" | |
try: | |
response_text = query_llm_with_prompt(prompt) | |
json_match = re.search(r'\{.*\}', response_text, re.DOTALL) | |
if json_match: | |
return json.loads(json_match.group()) | |
# ๋ถ์ ์คํจ ์ ํผ์ ํ๋ ํ๋์ผ๋ก ๊ฐ์ฃผ | |
return {"action_type": "SOLO_ACTION", "target": None, "topic": plan_step} | |
except Exception: | |
return {"action_type": "SOLO_ACTION", "target": None, "topic": plan_step} | |
def _query_llm_for_json_robustly(prompt: str) -> dict | list: | |
"""LLM์ JSON์ ์์ฒญํ๊ณ , ์คํจ ์ ์๊ฐ ๊ต์ ์ ์๋ํ๋ '๊ฐ๋์ธ' ํจ์""" | |
# 1๋จ๊ณ: LLM์ JSON ํ์ ์์ฒญ | |
response_text = query_llm_with_prompt(prompt) | |
# Markdown ์ฝ๋ ๋ธ๋ก(```json ... ```) ์์ ๋ด์ฉ๋ถํฐ ์ถ์ถ ์๋ | |
markdown_match = re.search(r'```(?:json)?\s*(\{.*\}|\[.*\])\s*```', response_text, re.DOTALL) | |
if markdown_match: | |
text_to_parse = markdown_match.group(1) # ์ฝ๋ ๋ธ๋ก ์์ ๋ด์ฉ๋ง ์ฌ์ฉ | |
else: | |
# Markdown์ด ์์ผ๋ฉด, ์ ์ฒด ํ ์คํธ์์ JSON์ ์ง์ ์ฐพ์ | |
# ์๋ต์ด Markdown ์ฝ๋ ๋ธ๋ก(```json...```)์ผ๋ก ๊ฐ์ธ์ฌ ์์ ๊ฒฝ์ฐ๋ฅผ ๋๋น | |
json_match = re.search(r'\{.*\}|\[.*\]', response_text, re.DOTALL) | |
if json_match: | |
text_to_parse = json_match.group(0) | |
else: | |
text_to_parse = response_text # ์ตํ์ ๊ฒฝ์ฐ ์๋ณธ ํ ์คํธ ์ฌ์ฉ | |
# ๋ถ๋ฆฌ์ธ ๊ฐ์ ๋์๋ฌธ์๋ฅผ ํ์ค์ ๋ง๊ฒ ์์ (True -> true) | |
text_to_parse = text_to_parse.replace(": True", ": true").replace(": False", ": false") | |
# ๋์ ๋๋ฆฌ๋ ๋ฆฌ์คํธ์ ๋ง์ง๋ง ์์ ๋ค์ ๋ถ์ ๊ผฌ๋ฆฌํ ์ผํ ์ ๊ฑฐ | |
text_to_parse = re.sub(r',\s*(\}|\])', r'\1', text_to_parse) | |
# ์ถ์ถ๋ ํ ์คํธ๋ก ํ์ฑ ๋ฐ ์๊ฐ ๊ต์ ์๋ | |
try: | |
return json.loads(text_to_parse) # ํ์ฑ ์ฑ๊ณต ์ ์ฆ์ ๋ฐํ | |
except json.JSONDecodeError: | |
# 3๋จ๊ณ: ํ์ฑ ์คํจ ์, LLM์๊ฒ '์๊ฐ ๊ต์ ' ์์ฒญ | |
correction_prompt = f""" | |
# Instruction | |
The following text is not a valid JSON. | |
Correct the syntax errors and return ONLY the valid JSON object or array. | |
Do not include any other text, explanations, or markdown. | |
# Invalid Text | |
"{response_text}" | |
# Corrected JSON: | |
""" | |
corrected_response = query_llm_with_prompt(correction_prompt) | |
# ์๊ฐ ๊ต์ ๋ ํ ์คํธ๋ ํ๋ฒ ๋ ์ ๋ฆฌ | |
corrected_response = corrected_response.replace(": True", ": true").replace(": False", ": false") | |
corrected_response = re.sub(r',\s*(\}|\])', r'\1', corrected_response) | |
try: | |
return json.loads(corrected_response) # ๊ต์ ๋ ์๋ต ํ์ฑ ์๋ | |
except json.JSONDecodeError: | |
# 4๋จ๊ณ: ์ต์ข ์คํจ ์, ์์ ํ ๊ธฐ๋ณธ๊ฐ ๋ฐํ | |
print(f"LLM ์๊ฐ ๊ต์ ์คํจ. ์๋ณธ ์๋ต: {response_text}") | |
return {} | |
def generate_plan_from_goal(npc_name: str, goal: str) -> List[str]: | |
"""๋ชฉํ๊ฐ ์ฃผ์ด์ง๋ฉด, ๊ฐ๋์ธ ํจ์๋ฅผ ์ด์ฉํด ์์ ์ ์ผ๋ก ๊ณํ์ ์์ฑ""" | |
prompt = f""" | |
# Instruction | |
- ๋น์ ์ '{npc_name}'์ด๋ผ๋ NPC๋ฅผ ์ํ AI ์กฐ์์ ๋๋ค. | |
- ์ด NPC์ ๋ชฉํ๋ "{goal}" ์ ๋๋ค. | |
- ์ด ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๊ธฐ ์ํ 3~5๋จ๊ณ์ ๊ตฌ์ฒด์ ์ธ ํ๋ ๊ณํ์ ๋ง๋ค์ด์ฃผ์ธ์. | |
- ๋น์ ์ ์๋ต์ ๋ฐ๋์ ๊ฐ ๋จ๊ณ๊ฐ ๋ฌธ์์ด๋ก ์ด๋ฃจ์ด์ง ์ ํจํ JSON ๋ฐฐ์ด์ด์ด์ผ ํฉ๋๋ค. | |
- ํ๋ ๊ณํ์ ๋ํ ๋ด์ฉ๋ง ์์ฑํด์ฃผ์ธ์. ('1๋จ๊ณ'์ ๊ฐ์ ๋ถํ์ํ ๋จ์ด๋ ํ์์์ต๋๋ค.) | |
# ๊ณํ (๋ฌธ์์ด๋ก ๊ตฌ์ฑ๋ JSON ๋ฐฐ์ด ํ์): | |
""" | |
result = _query_llm_for_json_robustly(prompt) | |
if isinstance(result, list) and all(isinstance(s, str) for s in result): | |
return result | |
return ["[๊ณํ ์์ฑ์ ์คํจํ์ต๋๋ค]"] # ์ต์ข ์คํจ ์ ์์ ํ ๊ฐ ๋ฐํ | |
def generate_dialogue_action(speaker: "NPC", target: "NPC", conversation: "Conversation", | |
memories: List["Memory"], is_final_turn: bool = False) -> dict: | |
"""NPC์ ๋ชจ๋ ๋ด๋ฉด ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋ค์ ๋์ฌ์ ํ๋์ ์์ฑ.""" | |
history_str = "\n".join(conversation.conversation_history) | |
memory_str = "\n".join([f"- {m.content}" for m in memories]) if memories else "๊ด๋ จ๋ ํน๋ณํ ๊ธฐ์ต ์์" | |
# ํํ ์ ๋ณด ์์ฑ | |
reputation_info = "" | |
reputation_score = speaker.reputation.get(target.name, 0) | |
if reputation_score > 5: | |
reputation_info = f"- '{target.korean_name}'์ ๋ํ ๋์ ํํ: ์ ๋ขฐํ ๋งํ ์ฌ๋ ๊ฐ๋ค. (์ ์: {reputation_score})" | |
elif reputation_score < -5: | |
reputation_info = f"- '{target.korean_name}'์ ๋ํ ๋์ ํํ: ์ข์ง ์์ ์๋ฌธ์ ๋ค์๋ค. (์ ์: {reputation_score})" | |
# ์์ง ๊ธฐ์ต ์กฐํ | |
symbolic_memories = [mem for mem in speaker.memory_store.get_all_memories() if mem.memory_type == "Symbolic"] | |
symbolic_section = "" | |
if symbolic_memories: | |
symbolic_section_title = "# Core Symbolic Values (Symbolic Memory)" | |
symbolic_contents = "\n".join([f"- {mem.content}" for mem in symbolic_memories]) | |
# ํ๋กฌํํธ์ ์์๊ฒ ๋ค์ด๊ฐ ์ ์๋๋ก ์๋ค๋ก ์ค๋ฐ๊ฟ ์ถ๊ฐ | |
symbolic_section = f"\n{symbolic_section_title}\n{symbolic_contents}" | |
# ์๋๋ฐฉ์ ๋ํด ์๊ณ ์๋ ์ ๋ณด๋ฅผ ํ๋กฌํํธ์ ์ถ๊ฐ | |
known_info_summary = "์์ง ์๋ ์ ๋ณด๊ฐ ์์" | |
if target.name in speaker.knowledge: | |
known_facts = [] | |
if "job" in speaker.knowledge[target.name]: | |
known_facts.append(f"์ง์ ์ {speaker.knowledge[target.name]['job']}์ด๋ค.") | |
if "age" in speaker.knowledge[target.name]: | |
known_facts.append(f"๋์ด๋ {speaker.knowledge[target.name]['age']}์ด๋ค.") | |
if known_facts: | |
known_info_summary = ", ".join(known_facts) | |
if is_final_turn: | |
task_instruction = """ | |
- ์๋๋ฐฉ์ด ๋ํ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ ค๊ณ ํฉ๋๋ค. ์๋๋ฐฉ์ ๋ง์ง๋ง ๋ง์ ์ด์ธ๋ฆฌ๋ ์์ฐ์ค๋ฌ์ด ์๋ณ ์ธ์ฌ๋ฅผ ์์ฑํ์ธ์. (์: "๋ค, ์กฐ์ฌํ ๊ฐ์ธ์.", "๋ค์์ ๋ ๋ดฌ์.", "์๊ฒ ์ต๋๋ค. ์ข์ ํ๋ฃจ ๋ณด๋ด์ธ์.") | |
- ์ด ๋์ฌ๋ฅผ ๋์ผ๋ก ๋ํ๋ฅผ ๋ง๋ฌด๋ฆฌ("END")ํ์ธ์. | |
""" | |
else: | |
task_instruction = """ | |
- ์ ๋ํ์ ํ๋ฆ๊ณผ ์ฃผ์ ์ ๋ง์ถฐ, ๋น์ ์ด ์ด๋ฒ ํด์ ํ ์์ฐ์ค๋ฌ์ด ๋์ฌ๋ฅผ ์์ฑํ์ธ์. | |
- ๋ํ๋ฅผ ๊ณ์ ์ด์ด๊ฐ์ง(`action: "CONTINUE"`), ์๋๋ฉด ์ด ๋์ฌ๋ฅผ ๋์ผ๋ก ๋ํ๋ฅผ ๋ง๋ฌด๋ฆฌํ ์ง(`action: "END"`) ๊ฒฐ์ ํ์ธ์. | |
- ๋ง์ฝ ์๋๋ฐฉ์ ๋ง์ ์์์ ๊ฐ์ด ์๋ณ ์ธ์ฌ ๋๋ ๋ํ ์ข ๋ฃ๋ฅผ ์๋ฏธํ๋ค๋ฉด, ๋ฐ๋์ ๋น์ ๋ ๊ทธ์ ๋ง๋ ์๋ณ ์ธ์ฌ๋ฅผ ํ๊ณ "action"์ "END"๋ก ์ค์ ํฉ๋๋ค. (์์: "๊ทธ๋ผ ์ด๋ง.", "๋ค์์ ๋ ๋ดฌ์.", "์๊ฒ ์ต๋๋ค. ์กฐ์ฌํ๊ฐ์ธ์.", "๋ค ๊ฐ์ฌํฉ๋๋ค.", "์๋ ํ ๊ฐ์ธ์.", "์๋ ํ ๊ณ์ธ์.") | |
- **๋ํ๋ฅผ ๋๋ด๊ธฐ๋ก ๊ฒฐ์ ํ๋ค๋ฉด (`action: "END"`), ๋น์ ์ ๋์ฌ๋ ๋ฐ๋์ "์ด์ ๊ฐ๋ด์ผ๊ฒ ์ด์", "๋ค์์ ๋ ์ด์ผ๊ธฐ ๋๋ ์" ์ ๊ฐ์ด ๋ํ์ ๋ง๋ฌด๋ฆฌ๋ฅผ ์์ํ๋ ๋ด์ฉ์ด์ด์ผ ํฉ๋๋ค.** | |
- ๋จ์ํ ์๋๋ฐฉ์ ๋ง์ ๋์๋ง ํ์ง ๋ง๊ณ , ๋น์ ์ ๊ธฐ์ต์ด๋ ์ฑ๊ฒฉ์ ๋ฐํ์ผ๋ก ์๋ก์ด ์๊ฐ์ด๋ ์ง๋ฌธ, ํ์ ๋ฅผ ๊บผ๋ด์ด ๋ํ๋ฅผ ํ๋ถํ๊ฒ ๋ง๋์ธ์. | |
""" | |
# ๋ํ ์ ๋ต(Conversation Strategies) ํ๋กฌํํธ ๋์ | |
prompt = f""" | |
# Persona | |
๋น์ ์ ๋ค์ ํ๋กํ์ ๊ฐ์ง "{speaker.korean_name}"์ด๋ผ๋ ์ธ๋ฌผ์ ๋๋ค. | |
- ์ง์ : {speaker.job} | |
- ์ฑ๊ฒฉ: {speaker.personality.get_narrative_summary()} | |
๋น์ ์ ์ง๊ธ "{target.korean_name}"์ ๋ํํ๊ณ ์์ต๋๋ค. | |
๋น์ ์ ์ฑ๊ฒฉ๊ณผ ์๋๋ฐฉ๊ณผ์ ๊ด๊ณ, ๊ด๋ จ๋ ๊ณผ๊ฑฐ ๊ธฐ์ต, ๊ทธ๋ฆฌ๊ณ ์ง๊ธ๊น์ง์ ๋ํ ํ๋ฆ์ ๋ชจ๋ ๊ณ ๋ คํ์ฌ ๊ฐ์ฅ ํ์ค์ ์ด๊ณ ๊น์ด ์๋ ๋ค์ ๋์ฌ๋ฅผ ๋ง๋์ธ์. | |
์๋ ์ค์ ์ ์๋ฒฝํ๊ฒ ๋ชฐ์ ํ์ฌ ๋๋ตํ์ธ์. | |
# Inner State | |
- "{target.korean_name}"์์ ๊ด๊ณ: {speaker.relationships.get_relationship_summary(target.name)} | |
- {reputation_info} | |
- "{target.korean_name}"์ ๋ํด ๋ด๊ฐ ์๋ ์ ๋ณด: {known_info_summary} | |
{symbolic_section} | |
# Relevant Memories | |
{memory_str} | |
# Conversation Context | |
- ํ์ฌ ๋ํ ์ฃผ์ : "{conversation.topic}" | |
- ์ง๊ธ๊น์ง์ ๋ํ ๋ด์ฉ: | |
{history_str} | |
# Instruction | |
{task_instruction} | |
- ๋น์ ์ ์๋ต์ ๋ฐ๋์ ์๋ JSON ํ์์ด์ด์ผ ํฉ๋๋ค. | |
- ๋์ฌ์๋ ์ด๋ฆ, ํ๋ ๋ฌ์ฌ, ๋ฐ์ดํ๋ฅผ ์ ๋ ํฌํจํ์ง ๋ง์ธ์. | |
- 'Relevant Memories'์ ๋น์ ์ ์๊ฐ๊ณผ ๊ฐ์ ์ ๋ฐฐ๊ฒฝ์ด ๋๋ ์ ๋ณด์ ๋๋ค. ๊ธฐ์ต์ ๋ด์ฉ ์์ฒด๋ฅผ ๊ทธ๋๋ก ๋งํ๋ ๊ฒ์ด ์๋๋ผ, ๊ทธ ๊ธฐ์ต์ ํตํด ํ์ฑ๋ ๋น์ ์ ์๊ฐ์ด๋ ๋๋์ ๋ํ์ ์์ฐ์ค๋ฝ๊ฒ ๋ น์ฌ๋ด์ธ์. | |
- ๋ง์ฝ 'Relevant Memories'์ด ํ์ฌ ๋ํ ์๋('{target.korean_name}')์ ์ง์ ์ ์ธ ๊ด๋ จ์ด ์๋ ๋ค๋ฅธ ์ฌ๋์ ๋ํ ๊ฒ์ด๋ผ๋ฉด, ์ ๋๋ก ํ์ฌ ๋ํ ์๋์๊ฒ ์ผ์ด๋ ์ผ์ธ ๊ฒ์ฒ๋ผ ์ฐฉ๊ฐํด์ ๋งํ์ง ๋ง์ธ์.** (์: ๋ฐฅ์ ๋ํ ๊ธฐ์ต์ ํ๋ ์ด์ด์๊ฒ ๋งํ ๋๋ "์ฌ์ค ์ผ๋ง ์ ์ ๋ฐฅ ์จ ๋๋ฌธ์..." ์ ๊ฐ์ด ๋ช ํํ๊ฒ ์ฃผ์ฒด๋ฅผ ๋ฐํ์ธ์.) | |
- ์๋์ 'Conversation Strategies'๊ณผ ๊ฐ์ด ํ์ฌ ๋ํ์ ๋งฅ๋ฝ๊ณผ ๋น์ ์ ์ฑ๊ฒฉ์ ๊ฐ์ฅ ์ ์ ํ ํ๋ ํ๋๋ฅผ ์ ํํ์ธ์. | |
# Conversation Strategies | |
1. **๊ณต๊ฐ ๋ฐ ์๋ก (Empathy & Comfort):** ์๋๋ฐฉ์ ๊ฐ์ ์ ๊ณต๊ฐํ๊ฑฐ๋ ๋ฐ๋ปํ ๋ง๋ก ์๋กํฉ๋๋ค. (์: "์ ๋ง ํ๋์ จ๊ฒ ์ด์. ์ ๋ผ๋ ๊ทธ๋ฌ์ ๊ฑฐ์์.") | |
2. **๊ฒฝํ ๊ณต์ (Share Experience):** ์์ ์ ๋น์ทํ ๊ฒฝํ์ ์ด์ผ๊ธฐํ๋ฉฐ ์ ๋๊ฐ์ ํ์ฑํฉ๋๋ค. (์: "์ ๋ ์์ ์ ๋น์ทํ ์ผ๋ก ๊ณ ์ํ ์ ์ด ์์ด์ ๊ทธ ๋ง์ ์ ์์์.") | |
3. **ํด๊ฒฐ์ฑ ์ ์ (Suggest Solution):** ๋ฌธ์ ์ํฉ์ ๋ํด ๊ตฌ์ฒด์ ์ธ ์กฐ์ธ์ด๋ ํด๊ฒฐ์ฑ ์ ์ ์ํฉ๋๋ค. (์: "ํน์ ์ด๋ ๊ฒ ํด๋ณด๋ ๊ฑด ์ด๋จ๊น์? ์ ๊ฐ ๋์๋๋ฆด ์๋ ์๊ณ ์.") | |
4. **์ฌํ ์ง๋ฌธ (Deeper Question):** ์๋๋ฐฉ์ ๋ง์ ๋ํด ๋ ๊น์ด ์ดํดํ๊ธฐ ์ํ ๊ตฌ์ฒด์ ์ธ ์ง๋ฌธ์ ํฉ๋๋ค. (์: "๊ทธ๋ ๊ฒ ์๊ฐํ๊ฒ ๋ ํน๋ณํ ๊ณ๊ธฐ๊ฐ ์์ผ์ จ๋์?") | |
5. **ํ์ ์ ํ (Change Topic):** ๋ํ๊ฐ ์ถฉ๋ถํ ์ด๋ฃจ์ด์ก๋ค๊ณ ์๊ฐ๋๋ฉด ์์ฐ์ค๋ฝ๊ฒ ๋ค๋ฅธ ์ฃผ์ ๋ก ๋์ด๊ฐ๋๋ค. (์: "๊ทธ๊ฑด ๊ทธ๋ ๊ณ , ํน์ ๋ค๋ฅธ ์ฌ๋ฏธ์๋ ์์์ ์๋์?") | |
6. **์ ๋จธ ์ฌ์ฉ (Use of Humor):** ๊ธด์ฅ๋ ๋ถ์๊ธฐ๋ฅผ ์ํํ๊ฑฐ๋ ๋ถ์๊ธฐ๋ฅผ ๋ถ๋๋ฝ๊ฒ ๋ฐ๊พธ๊ธฐ ์ํด ์์์ ์ ๋ํฉ๋๋ค. (์: "์ด์ฏค ๋๋ฉด ์ฐ๋ฆฌ ๋ ๋ค ์๊ฒฉ์ฆ ์์ด์ผ ํ๋ ๊ฑฐ ์๋๊ฐ์? ๊ณ ๋ฏผ ์ ๋ฌธ๊ฐ!) | |
7. **ํํผ ๋ฐ ๋ฌด์ (Avoidance/Deflection):** ๋ถํธํ๊ฑฐ๋ ๋ณต์กํ ์ฃผ์ ๋ฅผ ์ผ๋ถ๋ฌ ํผํ๊ฑฐ๋ ๋ฌด์ํฉ๋๋ค. (์: "๊ทธ ์๊ธด ๋ค์์ ํ์. ์ง๊ธ ๊ทธ ์๊ธฐ๋ฅผ ํ ๊ธฐ๋ถ ์๋์ผ.") | |
8. **๊ฐ์ ๋ฌด์ (Invalidation):** ์๋์ ๊ฐ์ ์ ์ฌ์ํ๊ฒ ์ฌ๊ธฐ๊ฑฐ๋ ์ธ์ ํ์ง ์์ต๋๋ค. (์: "๊ทธ ์ ๋ ๊ฐ์ง๊ณ ๋ญ ๊ทธ๋.") | |
9. **๋น๋/์ฑ ์ ์ ๊ฐ (Blame/Accusation):** ๋ฌธ์ ์ ์์ธ์ ์๋์๊ฒ ๋๋ฆฌ๋ฉฐ ๊ณต๊ฒฉ์ ์ผ๋ก ๋ฐ์ํฉ๋๋ค. (์: "๊ทธ๊ฑด ๋ ์๋ชป์ด์์. ์ ๋ํํ ๊ทธ๋?") | |
10. **๋น๊ผฌ๊ธฐ ๋ฐ ์กฐ๋กฑ (Sarcasm/Mockery):** ์ง์ ์ ์ผ๋ก ๊ณต๊ฒฉํ์ง ์์ง๋ง, ๋น๊ผฌ๋ ๋งํฌ๋ก ์๋๋ฅผ ๊น์๋ด๋ฆฝ๋๋ค. (์: "์ค, ์ด์ ์ผ ๊ทธ๊ฑธ ๊นจ๋ฌ์๋ค๊ณ ? ์ญ์ ๋น ๋ฅด์๋ค.") | |
11. **ํ์ ํํผ๋ฅผ ์ํ ๊ณผ๋ํ ์ผ๋ฐํ (Over-Generalization):** ๊ตฌ์ฒด์ ์ธ ๋ฌธ์ ์์ ๋ฒ์ด๋๊ธฐ ์ํด '์๋ ๋ค ๊ทธ๋' ์์ผ๋ก ์ผ๋ฐํ ํฉ๋๋ค. (์: "์ฌ๋๋ค์ด ๋ค ๊ทธ๋ ์ง ๋ญ. ๊ธฐ๋ํ์ง ๋ง.") | |
# Response (JSON format only) | |
{{ | |
"dialogue": "Conversation Strategies ์ ๋ต์ ๋ฐ๋ผ ์์ฑ๋ ์ค์ ๋งํ ๋์ฌ ๋ด์ฉ", | |
"action": "CONTINUE or END" | |
}} | |
""" | |
result = _query_llm_for_json_robustly(prompt) # 3์ค ๋ฐฉ์ด ์์คํ ์ฌํ์ฉ | |
# ๊ฒฐ๊ณผ ํฌ๋งท ๊ฒ์ฆ ๋ฐ ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ | |
if isinstance(result, dict) and "dialogue" in result and "action" in result: | |
# ํ์ฒ๋ฆฌ ๋ก์ง (์ด๋ฆ ์ค๋ณต์ด๋ ๋ฐ์ดํ ์ ๊ฑฐ) | |
dialogue = result.get("dialogue", "...").strip().strip('"') | |
name_prefix = f"{speaker.name}:" | |
if dialogue.startswith(name_prefix): | |
dialogue = dialogue[len(name_prefix):].strip().strip('"') | |
result["dialogue"] = dialogue | |
return result | |
return {"dialogue": "...", "action": "END"} # ์ต์ข ์คํจ ์ ์์ ํ๊ฒ ๋ํ ์ข ๋ฃ | |
def evaluate_social_action_outcome(initiator: "NPC", target: "NPC", action_type: str) -> dict: | |
"""๋ NPC์ ์ฑ๊ฒฉ๊ณผ ๊ด๊ณ๋ฅผ ๋ฐํ์ผ๋ก ์ฌํ์ ํ๋์ ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ก ํฉ๋๋ค.""" | |
prompt = f""" | |
# Context | |
- ํ๋์: "{initiator.korean_name}" (์ฑ๊ฒฉ: {initiator.personality.get_narrative_summary()}) | |
- ๋์์: "{target.korean_name}" (์ฑ๊ฒฉ: {target.personality.get_narrative_summary()}) | |
- ๋์ ๊ด๊ณ: {initiator.relationships.get_relationship_summary(target.name)} | |
- ํ๋: ํ๋์('{initiator.korean_name}')๋ ๋์์('{target.korean_name}')๋ฅผ ๋์์ฃผ๋ ค๊ณ ํฉ๋๋ค. (Action: {action_type}) | |
# Your Task | |
์ ์ํฉ์ ๋ฐํ์ผ๋ก, ์ด ํ๋์ ๊ฒฐ๊ณผ๋ฅผ ํ์ค์ ์ผ๋ก ์ถ๋ก ํด์ฃผ์ธ์. | |
๊ฒฐ๊ณผ๋ ๋ฐ๋์ ์๋ JSON ํ์์ด์ด์ผ ํฉ๋๋ค. | |
# Outcome (JSON format only) | |
{{ | |
"success": "True or False" | |
"outcome_summary": "ํ๋์ ๊ฒฐ๊ณผ๋ฅผ ํ ๋ฌธ์ฅ์ผ๋ก ์์ฝ (์: '๋ฐฅ์ ์ฐฐ๋ฆฌ์ ์ํฌ๋ฅธ ๋์์ ๊ณ ๋ง์ํ๋ฉด์๋ ์กฐ๊ธ ๋ต๋ตํดํ๋ค.')", | |
"initiator_emotion": "ํ๋์ ํ ํ ํ๋์๊ฐ ๋๋ ๊ฐ์ (์๋ 'Emotion_List์์ ์ฐพ์์ ํ ๋จ์ด๋ก ํํ)", | |
"target_emotion": "ํ๋์ ๋ฐ์ ํ ๋์์๊ฐ ๋๋ ๊ฐ์ (์๋ 'Emotion_List์์ ์ฐพ์์ ํ ๋จ์ด๋ก ํํ)" | |
}} | |
# Emotion_List | |
{EMOTION_LIST} | |
""" | |
result = _query_llm_for_json_robustly(prompt) # 3์ค ๋ฐฉ์ด ์์คํ ์ฌํ์ฉ | |
if isinstance(result, dict) and "success" in result: | |
return result | |
return {# ์ต์ข ์คํจ ์ ์์ ํ ๊ธฐ๋ณธ๊ฐ ๋ฐํ | |
"success": False, | |
"outcome_summary": "ํ๋์ ๊ฒฐ๊ณผ๋ฅผ ํ๊ฐํ๋ ๋ฐ ์คํจํ์ต๋๋ค.", | |
"initiator_emotion": "confusion", | |
"target_emotion": "neutral" | |
} | |
def evaluate_goal_achievement(initiator_name: str, goal: str, conversation_transcript: str) -> dict: | |
"""๋ํ์ ๋ชฉํ์ ์ ์ฒด ๋ด์ฉ์ ๋ฐํ์ผ๋ก, ๋ชฉํ ๋ฌ์ฑ ์ฌ๋ถ๋ฅผ ํ๊ฐ""" | |
prompt = f""" | |
# Context | |
- ๋ํ ์์์: "{initiator_name}" | |
- ๋ํ ์์์์ ๋ชฉํ (๋ํ ์ฃผ์ ): "{goal}" | |
- ์ค์ ๋ํ ๋ด์ฉ: | |
{conversation_transcript} | |
# Your Task | |
์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก, ๋ํ ์์์("{initiator_name}")๊ฐ ์์ ์ ์๋ ๋ชฉํ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ฌ์ฑํ๋์ง ํ๊ฐํด์ฃผ์ธ์. | |
๊ฒฐ๊ณผ๋ ๋ฐ๋์ ์๋ JSON ํ์์ด์ด์ผ ํฉ๋๋ค. | |
# Evaluation (JSON format only) | |
{{ | |
"goal_achieved": True, | |
"reason": "๋ชฉํ ๋ฌ์ฑ ๋๋ ์คํจ์ ๋ํ ๊ฐ๊ฒฐํ ์ด์ (์: '์๋๋ฐฉ์ ์๋กํ๊ณ ์ง์งํ๋ฉฐ ๊ธ์ ์ ์ธ ๋ฐ์์ ์ด๋์ด๋๋ค.)", | |
"initiator_emotion": "์ด ๊ฒฐ๊ณผ๋ฅผ ์ป์ ํ ๋ํ ์์์๊ฐ ๋๋ ๊ฐ์ (์๋ 'Emotion_List์์ ์ฐพ์์ ํ ๋จ์ด๋ก ํํ) | |
}} | |
# Emotion_List | |
{EMOTION_LIST} | |
""" | |
result = _query_llm_for_json_robustly(prompt) | |
if isinstance(result, dict) and "goal_achieved" in result: | |
return result | |
return { | |
# ์ต์ข ์คํจ ์ ์์ ํ ๊ธฐ๋ณธ๊ฐ ๋ฐํ | |
"goal_achieved": False, | |
"reason": "๋ชฉํ ๋ฌ์ฑ ์ฌ๋ถ ํ๊ฐ์ ์คํจํ์ต๋๋ค.", | |
"initiator_emotion": "confusion" | |
} | |
def generate_reflection_topic(memories: List["Memory"], personality_summary: str) -> str: | |
"""์ต๊ทผ ๊ธฐ์ต๊ณผ ์ฑ๊ฒฉ์ ๋ฐํ์ผ๋ก, ๊น์ด ์๋ ์ฑ์ฐฐ ์ฃผ์ ๋ฅผ ๋์ ์ผ๋ก ์์ฑ""" | |
if not memories: | |
return "๋์ ์ธ์์์ ๊ฐ์ฅ ์ค์ํ๋ ์๊ฐ์ ์ธ์ ์์๊น?" | |
# ๊ธฐ์ต ๋ชฉ๋ก์ ํ๋กฌํํธ์ ๋ฃ๊ธฐ ์ข์ ํํ๋ก ๋ณํ | |
memory_details = "\n".join([f"- {mem.content} (๊ฐ์ : {mem.emotion})" for mem in memories]) | |
prompt = f""" | |
# Instructions | |
- ์๋ 'Personality'์ 'Recent Memory List'๋ ๊ฐ๊ฐ ์ด๋ค ์ฌ๋์ '์ฑ๊ฒฉ'๊ณผ '์ต๊ทผ ๊ธฐ์ต ๋ชฉ๋ก'์ ๋๋ค. | |
- ์ด ๊ธฐ์ต๋ค์ ์ข ํฉ์ ์ผ๋ก ๋ถ์ํ์ฌ, ์ด ์ฌ๋์ด ์ง๊ธ ์์ ์ ๋ํด ๊น์ด ์ฑ์ฐฐํด๋ณผ ๋งํ ๊ฐ์ฅ ์๋ฏธ ์๋ '์ง๋ฌธ'์ ํ๊ตญ์ด๋ก ํ ๋ฌธ์ฅ ๋ง๋ค์ด์ฃผ์ธ์. | |
# Personality | |
{personality_summary} | |
# Recent Memory List | |
{memory_details} | |
# Self-Reflective Question (in Korean, as a single sentence): | |
""" | |
topic = query_llm_with_prompt(prompt).strip() | |
return topic if topic and "[LLM Error]" not in topic else "๋์ ์ธ์์์ ๊ฐ์ฅ ์ค์ํ ๊ฐ์น๋ ๋ฌด์์ผ๊น?" |