m97j commited on
Commit
bbb241c
Β·
1 Parent(s): c915682

test case update

Browse files
config.py CHANGED
@@ -17,7 +17,7 @@ MAX_LENGTH = int(os.getenv("MAX_LENGTH", 1024))
17
  NUM_FLAGS = int(os.getenv("NUM_FLAGS", 7)) # flags.json 길이와 일치
18
 
19
  # 생성 νŒŒλΌλ―Έν„°
20
- GEN_MAX_NEW_TOKENS = int(os.getenv("GEN_MAX_NEW_TOKENS", 200))
21
  GEN_TEMPERATURE = float(os.getenv("GEN_TEMPERATURE", 0.7))
22
  GEN_TOP_P = float(os.getenv("GEN_TOP_P", 0.9))
23
 
 
17
  NUM_FLAGS = int(os.getenv("NUM_FLAGS", 7)) # flags.json 길이와 일치
18
 
19
  # 생성 νŒŒλΌλ―Έν„°
20
+ GEN_MAX_NEW_TOKENS = int(os.getenv("GEN_MAX_NEW_TOKENS", 400))
21
  GEN_TEMPERATURE = float(os.getenv("GEN_TEMPERATURE", 0.7))
22
  GEN_TOP_P = float(os.getenv("GEN_TOP_P", 0.9))
23
 
flags.json CHANGED
@@ -2,7 +2,7 @@
2
  "ALL_FLAGS": [
3
  "give_item",
4
  "give_hint",
5
- "quest_stage_change",
6
  "change_game_state",
7
  "change_player_state",
8
  "npc_action",
 
2
  "ALL_FLAGS": [
3
  "give_item",
4
  "give_hint",
5
+ "change_npc_state",
6
  "change_game_state",
7
  "change_player_state",
8
  "npc_action",
inference.py CHANGED
@@ -3,7 +3,7 @@ from config import DEVICE, MAX_LENGTH, GEN_MAX_NEW_TOKENS, GEN_TEMPERATURE, GEN_
3
  from model_loader import ModelWrapper
4
 
5
  # μ „μ—­ λ‘œλ“œ (μ„œλ²„ μ‹œμž‘ μ‹œ 1회)
6
- wrapper = ModelWrapper()
7
  tokenizer, model, flags_order = wrapper.get()
8
 
9
  GEN_PARAMS = {
@@ -18,14 +18,17 @@ def run_inference(prompt: str):
18
  inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=MAX_LENGTH).to(DEVICE)
19
 
20
  with torch.no_grad():
 
21
  gen_ids = model.generate(**inputs, **GEN_PARAMS)
22
  generated_text = tokenizer.decode(
23
  gen_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
24
  )
25
 
 
26
  outputs = model(**inputs, output_hidden_states=True)
27
  h = outputs.hidden_states[-1]
28
 
 
29
  STATE_ID = tokenizer.convert_tokens_to_ids("<STATE>")
30
  ids = inputs["input_ids"]
31
  mask = (ids == STATE_ID).unsqueeze(-1)
@@ -35,6 +38,7 @@ def run_inference(prompt: str):
35
  else:
36
  pooled = h[:, -1, :]
37
 
 
38
  delta_pred = torch.tanh(model.delta_head(pooled))[0].cpu().tolist()
39
  flag_prob = torch.sigmoid(model.flag_head(pooled))[0].cpu().tolist()
40
  flag_thr = torch.sigmoid(model.flag_threshold_head(pooled))[0].cpu().tolist()
 
3
  from model_loader import ModelWrapper
4
 
5
  # μ „μ—­ λ‘œλ“œ (μ„œλ²„ μ‹œμž‘ μ‹œ 1회)
6
+ wrapper = ModelWrapper() # 기본은 latest 브랜치
7
  tokenizer, model, flags_order = wrapper.get()
8
 
9
  GEN_PARAMS = {
 
18
  inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=MAX_LENGTH).to(DEVICE)
19
 
20
  with torch.no_grad():
21
+ # ν…μŠ€νŠΈ 생성
22
  gen_ids = model.generate(**inputs, **GEN_PARAMS)
23
  generated_text = tokenizer.decode(
24
  gen_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
25
  )
26
 
27
+ # νžˆλ“  μŠ€ν…Œμ΄νŠΈ μΆ”μΆœ
28
  outputs = model(**inputs, output_hidden_states=True)
29
  h = outputs.hidden_states[-1]
30
 
31
+ # <STATE> 토큰 μœ„μΉ˜ 풀링
32
  STATE_ID = tokenizer.convert_tokens_to_ids("<STATE>")
33
  ids = inputs["input_ids"]
34
  mask = (ids == STATE_ID).unsqueeze(-1)
 
38
  else:
39
  pooled = h[:, -1, :]
40
 
41
+ # μ»€μŠ€ν…€ ν—€λ“œ μΆ”λ‘ 
42
  delta_pred = torch.tanh(model.delta_head(pooled))[0].cpu().tolist()
43
  flag_prob = torch.sigmoid(model.flag_head(pooled))[0].cpu().tolist()
44
  flag_thr = torch.sigmoid(model.flag_threshold_head(pooled))[0].cpu().tolist()
model_loader.py CHANGED
@@ -1,10 +1,7 @@
1
  import os, json, torch
2
  import torch.nn as nn
3
  from transformers import AutoTokenizer, AutoModelForCausalLM
4
- from peft import PeftModel
5
- from config import BASE_MODEL, ADAPTERS, DEVICE, HF_TOKEN
6
-
7
- ADAPTER_VOCAB_SIZE = 151672 # ν•™μŠ΅ μ‹œμ  vocab size (둜그 κΈ°μ€€)
8
 
9
  SPECIALS = ["<SYS>", "<CTX>", "<PLAYER>", "<NPC>", "<STATE>", "<RAG>", "<PLAYER_STATE>"]
10
 
@@ -21,9 +18,12 @@ class ModelWrapper:
21
  self.flags_order = json.load(open(flags_path, encoding="utf-8"))["ALL_FLAGS"]
22
  self.num_flags = len(self.flags_order)
23
 
24
- # 1) ν† ν¬λ‚˜μ΄μ € (ν•™μŠ΅κ³Ό 동일 μ˜΅μ…˜ + SPECIALS)
 
 
25
  self.tokenizer = AutoTokenizer.from_pretrained(
26
- BASE_MODEL,
 
27
  use_fast=True,
28
  token=HF_TOKEN,
29
  trust_remote_code=True
@@ -31,39 +31,25 @@ class ModelWrapper:
31
  if self.tokenizer.pad_token is None:
32
  self.tokenizer.pad_token = self.tokenizer.eos_token
33
  self.tokenizer.padding_side = "right"
34
- # ν•™μŠ΅ μ‹œ μΆ”κ°€ν–ˆλ˜ 특수 토큰 μž¬ν˜„
35
  self.tokenizer.add_special_tokens({"additional_special_tokens": SPECIALS})
36
 
37
- # 2) 베이슀 λͺ¨λΈ (μ˜€ν”„λ‘œλ”© 끄고 λ‘œλ“œ)
38
- base = AutoModelForCausalLM.from_pretrained(
39
- BASE_MODEL,
40
- device_map=None, # βœ… μ˜€ν”„λ‘œλ”© λΉ„ν™œμ„±ν™”
41
- low_cpu_mem_usage=False, # βœ… meta ν…μ„œ 생성 λ°©μ§€
42
- trust_remote_code=True,
43
- token=HF_TOKEN
44
- )
45
-
46
- # 3) ν•™μŠ΅ μ‹œ vocab size둜 κ°•μ œ λ¦¬μ‚¬μ΄μ¦ˆ (μ–΄λŒ‘ν„° λ‘œλ“œ 전에)
47
- base.resize_token_embeddings(ADAPTER_VOCAB_SIZE)
48
-
49
- # 4) LoRA μ–΄λŒ‘ν„° 적용 (μ˜€ν”„λ‘œλ”© 끄고 λ‘œλ“œ)
50
- branch = get_current_branch()
51
- self.model = PeftModel.from_pretrained(
52
- base,
53
- ADAPTERS,
54
  revision=branch,
55
- device_map=None, # βœ… μ˜€ν”„λ‘œλ”© λΉ„ν™œμ„±ν™”
56
- low_cpu_mem_usage=False, # βœ… meta ν…μ„œ 생성 λ°©μ§€
 
57
  token=HF_TOKEN
58
  )
59
 
60
- # 5) μ»€μŠ€ν…€ ν—€λ“œ
61
  hidden_size = self.model.config.hidden_size
62
  self.model.delta_head = nn.Linear(hidden_size, 2).to(DEVICE)
63
  self.model.flag_head = nn.Linear(hidden_size, self.num_flags).to(DEVICE)
64
  self.model.flag_threshold_head = nn.Linear(hidden_size, self.num_flags).to(DEVICE)
65
 
66
- # 6) μ»€μŠ€ν…€ ν—€λ“œ κ°€μ€‘μΉ˜ λ‘œλ“œ(μžˆμ„ 경우)
67
  for head_name, file_name in [
68
  ("delta_head", "delta_head.pt"),
69
  ("flag_head", "flag_head.pt"),
@@ -77,7 +63,7 @@ class ModelWrapper:
77
  except Exception as e:
78
  print(f"[WARN] Failed to load {file_name}: {e}")
79
 
80
- # 7) λ””λ°”μ΄μŠ€ 배치
81
  self.model.to(DEVICE)
82
  self.model.eval()
83
 
 
1
  import os, json, torch
2
  import torch.nn as nn
3
  from transformers import AutoTokenizer, AutoModelForCausalLM
4
+ from config import DEVICE, HF_TOKEN
 
 
 
5
 
6
  SPECIALS = ["<SYS>", "<CTX>", "<PLAYER>", "<NPC>", "<STATE>", "<RAG>", "<PLAYER_STATE>"]
7
 
 
18
  self.flags_order = json.load(open(flags_path, encoding="utf-8"))["ALL_FLAGS"]
19
  self.num_flags = len(self.flags_order)
20
 
21
+ branch = get_current_branch()
22
+
23
+ # 1) ν† ν¬λ‚˜μ΄μ € (ν•™μŠ΅ λ‹Ήμ‹œ vocab + SPECIALS)
24
  self.tokenizer = AutoTokenizer.from_pretrained(
25
+ "m97j/npc_LoRA-fps", # λ³‘ν•©λœ λͺ¨λΈμ΄ μ˜¬λΌκ°„ repo
26
+ revision=branch,
27
  use_fast=True,
28
  token=HF_TOKEN,
29
  trust_remote_code=True
 
31
  if self.tokenizer.pad_token is None:
32
  self.tokenizer.pad_token = self.tokenizer.eos_token
33
  self.tokenizer.padding_side = "right"
 
34
  self.tokenizer.add_special_tokens({"additional_special_tokens": SPECIALS})
35
 
36
+ # 2) λ³‘ν•©λœ λͺ¨λΈ λ‘œλ“œ (μƒ€λ“œ μžλ™ 인식)
37
+ self.model = AutoModelForCausalLM.from_pretrained(
38
+ "m97j/npc_LoRA-fps",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  revision=branch,
40
+ device_map=None, # μ˜€ν”„λ‘œλ”© λΉ„ν™œμ„±ν™”
41
+ low_cpu_mem_usage=False,
42
+ trust_remote_code=True,
43
  token=HF_TOKEN
44
  )
45
 
46
+ # 3) μ»€μŠ€ν…€ ν—€λ“œ μΆ”κ°€
47
  hidden_size = self.model.config.hidden_size
48
  self.model.delta_head = nn.Linear(hidden_size, 2).to(DEVICE)
49
  self.model.flag_head = nn.Linear(hidden_size, self.num_flags).to(DEVICE)
50
  self.model.flag_threshold_head = nn.Linear(hidden_size, self.num_flags).to(DEVICE)
51
 
52
+ # 4) μ»€μŠ€ν…€ ν—€λ“œ κ°€μ€‘μΉ˜ λ‘œλ“œ
53
  for head_name, file_name in [
54
  ("delta_head", "delta_head.pt"),
55
  ("flag_head", "flag_head.pt"),
 
63
  except Exception as e:
64
  print(f"[WARN] Failed to load {file_name}: {e}")
65
 
66
+ # 5) λ””λ°”μ΄μŠ€ 배치
67
  self.model.to(DEVICE)
68
  self.model.eval()
69
 
modules/case_loader.py CHANGED
@@ -9,14 +9,18 @@ with open(TEST_CASES_PATH, "r", encoding="utf-8") as f:
9
  TEST_CASES = json.load(f)
10
 
11
  def get_case_names():
12
- return [f"{i+1}. {c['description']}" for i, c in enumerate(TEST_CASES)]
 
 
 
 
13
 
14
  def load_case(idx):
15
- case = TEST_CASES[idx]
16
- return json.dumps(case, ensure_ascii=False, indent=2), case["player_utterance"]
17
 
18
  def run_case(idx, player_utt):
19
- case = TEST_CASES[idx].copy()
20
  case["player_utterance"] = player_utt
21
  prompt = build_webtest_prompt(case["npc_id"], case["npc_location"], player_utt)
22
  result = run_inference(prompt)
 
9
  TEST_CASES = json.load(f)
10
 
11
  def get_case_names():
12
+ # description은 input μ•ˆμ— 있음
13
+ return [f"{i+1}. {c['input'].get('description','')}" for i, c in enumerate(TEST_CASES)]
14
+
15
+ def load_cases():
16
+ return TEST_CASES
17
 
18
  def load_case(idx):
19
+ cases = load_cases()
20
+ return cases[idx]
21
 
22
  def run_case(idx, player_utt):
23
+ case = TEST_CASES[idx]["input"].copy()
24
  case["player_utterance"] = player_utt
25
  prompt = build_webtest_prompt(case["npc_id"], case["npc_location"], player_utt)
26
  result = run_inference(prompt)
modules/ui_components.py CHANGED
@@ -1,9 +1,39 @@
1
  import gradio as gr
2
- from .case_loader import get_case_names, load_case, run_case
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  def build_ui():
5
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple")) as demo:
6
- # 상단 μ†Œκ°œ
7
  gr.Markdown("""
8
  # πŸ‘Ύ PersonaChatEngine HF-Serve
9
  **κ²Œμž„ λ‚΄ NPC 메인 λͺ¨λΈ μΆ”λ‘  μ„œλ²„**
@@ -13,31 +43,68 @@ def build_ui():
13
  with gr.Row():
14
  gr.Button("πŸ“„ 상세 λ¬Έμ„œ 보기",
15
  link="https://huggingface.co/spaces/m97j/PersonaChatEngine_HF-serve/blob/main/README.md")
16
- gr.Button("πŸ’» Colab ν…ŒμŠ€νŠΈ μ—΄κΈ°",
17
  link="https://colab.research.google.com/drive/1_-qH8kdoU2Jj58TdaSnswHex-BFefInq?usp=sharing#scrollTo=cFJGv8BJ8oPD")
18
 
19
  gr.Markdown("### 🎯 ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ 기반 간단 μ‹€ν–‰")
 
20
 
21
  with gr.Row():
22
- case_dropdown = gr.Dropdown(choices=get_case_names(), label="ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ 선택", value=get_case_names()[0])
23
  load_btn = gr.Button("μΌ€μ΄μŠ€ 뢈러였기")
24
 
25
- case_info = gr.Textbox(label="μΌ€μ΄μŠ€ 정보", lines=10)
26
- player_input = gr.Textbox(label="Player Utterance μˆ˜μ •", lines=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  run_btn = gr.Button("πŸš€ Run Inference", variant="primary")
29
  npc_resp = gr.Textbox(label="NPC Response")
30
  deltas = gr.JSON(label="Deltas")
31
  flags = gr.JSON(label="Flags Probabilities")
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  load_btn.click(
34
- fn=lambda name: load_case(get_case_names().index(name)),
35
  inputs=[case_dropdown],
36
- outputs=[case_info, player_input]
 
 
 
 
37
  )
38
 
 
39
  run_btn.click(
40
- fn=lambda name, utt: run_case(get_case_names().index(name), utt),
41
  inputs=[case_dropdown, player_input],
42
  outputs=[npc_resp, deltas, flags]
43
  )
 
1
  import gradio as gr
2
+ from .case_loader import load_case, run_case
3
+
4
+ # μ‹œμ—°μš© μΌ€μ΄μŠ€ 이름 ν•˜λ“œμ½”λ”©
5
+ CASE_NAMES = [
6
+ "폐곡μž₯μ—μ„œ NPC와 λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
7
+ "λ§ˆμ„ λŒ€μž₯μž₯이와 무기 μˆ˜λ¦¬μ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
8
+ "μˆ²μ† μ€λ‘”μžμ™€ 희귀 μ•½μ΄ˆμ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
9
+ "항ꡬ 관리관과 μΆœν•­ ν—ˆκ°€μ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
10
+ "λ§ˆλ²•μ‚¬ κ²¬μŠ΅μƒκ³Ό κ³ λŒ€ μ£Όλ¬Έμ„œμ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄"
11
+ ]
12
+
13
+ def format_case_info(case: dict) -> dict:
14
+ """μΌ€μ΄μŠ€ 정보λ₯Ό 보기 μ’‹κ²Œ μ •λ¦¬ν•΄μ„œ λ°˜ν™˜"""
15
+ inp = case["input"]
16
+ tags = inp.get("tags", {})
17
+ context_lines = [f"{h['role'].upper()}: {h['text']}" for h in inp.get("context", [])]
18
+
19
+ return {
20
+ "npc_id": inp.get("npc_id", ""),
21
+ "npc_location": inp.get("npc_location", ""),
22
+ "quest_stage": tags.get("quest_stage", ""),
23
+ "relationship": tags.get("relationship", ""),
24
+ "trust": tags.get("trust", ""),
25
+ "npc_mood": tags.get("npc_mood", ""),
26
+ "player_reputation": tags.get("player_reputation", ""),
27
+ "style": tags.get("style", ""),
28
+ "lore": inp.get("lore", ""),
29
+ "description": inp.get("description", ""),
30
+ "player_state": inp.get("player_state", {}),
31
+ "context": "\n".join(context_lines),
32
+ "player_utterance": inp.get("player_utterance", "")
33
+ }
34
 
35
  def build_ui():
36
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple")) as demo:
 
37
  gr.Markdown("""
38
  # πŸ‘Ύ PersonaChatEngine HF-Serve
39
  **κ²Œμž„ λ‚΄ NPC 메인 λͺ¨λΈ μΆ”λ‘  μ„œλ²„**
 
43
  with gr.Row():
44
  gr.Button("πŸ“„ 상세 λ¬Έμ„œ 보기",
45
  link="https://huggingface.co/spaces/m97j/PersonaChatEngine_HF-serve/blob/main/README.md")
46
+ gr.Button("πŸ’» Colab λ…ΈνŠΈλΆ μ—΄κΈ°",
47
  link="https://colab.research.google.com/drive/1_-qH8kdoU2Jj58TdaSnswHex-BFefInq?usp=sharing#scrollTo=cFJGv8BJ8oPD")
48
 
49
  gr.Markdown("### 🎯 ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ 기반 간단 μ‹€ν–‰")
50
+ gr.Markdown("⚠️ μΆ”λ‘ μ—λŠ” 수 초 ~ μ΅œλŒ€ 1λΆ„ 정도 μ†Œμš”λ  수 μžˆμŠ΅λ‹ˆλ‹€. μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”.")
51
 
52
  with gr.Row():
53
+ case_dropdown = gr.Dropdown(choices=CASE_NAMES, label="ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ 선택", value=CASE_NAMES[0])
54
  load_btn = gr.Button("μΌ€μ΄μŠ€ 뢈러였기")
55
 
56
+ # μΌ€μ΄μŠ€ 정보 ν‘œμ‹œ μ˜μ—­
57
+ with gr.Row():
58
+ with gr.Column():
59
+ npc_id = gr.Textbox(label="NPC ID", interactive=False)
60
+ npc_loc = gr.Textbox(label="NPC Location", interactive=False)
61
+ quest_stage = gr.Textbox(label="Quest Stage", interactive=False)
62
+ relationship = gr.Textbox(label="Relationship", interactive=False)
63
+ trust = gr.Textbox(label="Trust", interactive=False)
64
+ npc_mood = gr.Textbox(label="NPC Mood", interactive=False)
65
+ player_rep = gr.Textbox(label="Player Reputation", interactive=False)
66
+ style = gr.Textbox(label="Style", interactive=False)
67
+
68
+ with gr.Column():
69
+ lore = gr.Textbox(label="Lore", lines=3, interactive=False)
70
+ desc = gr.Textbox(label="Description", lines=3, interactive=False)
71
+ player_state = gr.JSON(label="Player State", interactive=False)
72
+ context = gr.Textbox(label="Context", lines=6, interactive=False)
73
+
74
+ # Player UtteranceλŠ” 별도 μž…λ ₯μ°½
75
+ player_input = gr.Textbox(label="Player Utterance", lines=2)
76
 
77
  run_btn = gr.Button("πŸš€ Run Inference", variant="primary")
78
  npc_resp = gr.Textbox(label="NPC Response")
79
  deltas = gr.JSON(label="Deltas")
80
  flags = gr.JSON(label="Flags Probabilities")
81
 
82
+ # μΌ€μ΄μŠ€ 뢈러였기 λ™μž‘
83
+ def on_load_case(name):
84
+ idx = CASE_NAMES.index(name)
85
+ case = load_case(idx) # TEST_CASES 직접 μ ‘κ·Ό λŒ€μ‹  load_case μ‚¬μš©
86
+ info = format_case_info(case)
87
+ return (
88
+ info["npc_id"], info["npc_location"], info["quest_stage"],
89
+ info["relationship"], info["trust"], info["npc_mood"],
90
+ info["player_reputation"], info["style"], info["lore"],
91
+ info["description"], info["player_state"], info["context"],
92
+ info["player_utterance"]
93
+ )
94
+
95
  load_btn.click(
96
+ fn=on_load_case,
97
  inputs=[case_dropdown],
98
+ outputs=[
99
+ npc_id, npc_loc, quest_stage, relationship, trust,
100
+ npc_mood, player_rep, style, lore, desc, player_state, context,
101
+ player_input
102
+ ]
103
  )
104
 
105
+ # μΆ”λ‘  μ‹€ν–‰
106
  run_btn.click(
107
+ fn=lambda name, utt: run_case(CASE_NAMES.index(name), utt),
108
  inputs=[case_dropdown, player_input],
109
  outputs=[npc_resp, deltas, flags]
110
  )
test_cases.json CHANGED
@@ -1,100 +1,143 @@
1
  [
2
  {
3
- "id": "case1",
4
- "npc_id": "mother_abandoned_factory",
5
- "npc_location": "map1",
6
- "description": "폐곡μž₯μ—μ„œ NPC와 λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
7
- "player_utterance": "μ•„! 머리가!!! κ°‘μžκΈ° 기얡이 λ– μ˜¬λžμ–΄μš”...",
8
- "tags": {
9
- "quest_stage": "in_progress",
10
- "relationship": 0.35,
11
- "trust": 0.35,
12
- "npc_mood": "grief",
13
- "player_reputation": "helpful",
14
- "style": "emotional"
15
- },
16
- "lore": "이 곡μž₯은 μˆ˜μ‹­ λ…„ μ „ ν™”μž¬λ‘œ νμ‡„λ˜μ—ˆλ‹€.",
17
- "context": [
18
- {"role": "player", "text": "사싀 이 곡μž₯을 λŒμ•„λ‹€λ‹ˆλ©΄μ„œ..."},
19
- {"role": "npc", "text": "ν˜Ήμ‹œ κ·Έ νŒŒν‹°μ— Jason도 μžˆμ—ˆλ‚˜μš”..."}
20
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  },
22
  {
23
- "id": "case2",
24
- "npc_id": "blacksmith_village_center",
25
- "npc_location": "village_square",
26
- "description": "λ§ˆμ„ λŒ€μž₯μž₯이와 무기 μˆ˜λ¦¬μ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
27
- "player_utterance": "이 검을 λ‹€μ‹œ μ“Έ 수 있게 고쳐쀄 수 μžˆλ‚˜μš”?",
28
- "tags": {
29
- "quest_stage": "not_started",
30
- "relationship": 0.2,
31
- "trust": 0.4,
32
- "npc_mood": "neutral",
33
- "player_reputation": "unknown",
34
- "style": "direct"
35
- },
36
- "lore": "λ§ˆμ„μ˜ λŒ€μž₯μž₯μ΄λŠ” μ„ΈλŒ€λ₯Ό 이어 무기λ₯Ό μ œμž‘ν•΄μ™”λ‹€.",
37
- "context": [
38
- {"role": "npc", "text": "였, μ—¬ν–‰μžκ΅°. 무슨 일둜 μ™”λ‚˜?"}
39
- ]
 
 
 
 
 
 
 
 
40
  },
41
  {
42
- "id": "case3",
43
- "npc_id": "forest_hermit",
44
- "npc_location": "deep_forest",
45
- "description": "μˆ²μ† μ€λ‘”μžμ™€ 희귀 μ•½μ΄ˆμ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
46
- "player_utterance": "ν˜Ήμ‹œ 이 κ·Όμ²˜μ—μ„œ ν‘Έλ₯ΈλΉ› μ•½μ΄ˆλ₯Ό λ³Έ 적 μžˆλ‚˜μš”?",
47
- "tags": {
48
- "quest_stage": "in_progress",
49
- "relationship": 0.5,
50
- "trust": 0.6,
51
- "npc_mood": "curious",
52
- "player_reputation": "friendly",
53
- "style": "polite"
54
- },
55
- "lore": "μ€λ‘”μžλŠ” μˆ²μ† κΉŠμ€ κ³³μ—μ„œ μ•½μ΄ˆμ™€ 버섯을 μ—°κ΅¬ν•œλ‹€.",
56
- "context": [
57
- {"role": "player", "text": "μ•ˆλ…•ν•˜μ„Έμš”, ν˜Ήμ‹œ μž μ‹œ 이야기 λ‚˜λˆŒ 수 μžˆμ„κΉŒμš”?"},
58
- {"role": "npc", "text": "μ—¬κΈ°κΉŒμ§€ μ˜€λŠ” μ‚¬λžŒμ€ λ“œλ¬Όμ§€μš”."}
59
- ]
 
 
 
 
 
 
 
60
  },
61
  {
62
- "id": "case4",
63
- "npc_id": "captain_port_authority",
64
- "npc_location": "harbor",
65
- "description": "항ꡬ 관리관과 μΆœν•­ ν—ˆκ°€μ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
66
- "player_utterance": "이 λ°°λ₯Ό 였늘 μ•ˆμ— μΆœν•­μ‹œμΌœμ•Ό ν•©λ‹ˆλ‹€. ν—ˆκ°€λ₯Ό λΆ€νƒλ“œλ¦½λ‹ˆλ‹€.",
67
- "tags": {
68
- "quest_stage": "urgent",
69
- "relationship": 0.45,
70
- "trust": 0.3,
71
- "npc_mood": "suspicious",
72
- "player_reputation": "neutral",
73
- "style": "persuasive"
74
- },
75
- "lore": "항ꡬ 관리관은 λͺ¨λ“  μ„ λ°•μ˜ μΆœν•­μ„ μ—„κ²©νžˆ κ΄€λ¦¬ν•œλ‹€.",
76
- "context": [
77
- {"role": "npc", "text": "μ„œλ₯˜λŠ” λ‹€ μ€€λΉ„λλ‚˜?"}
78
- ]
 
 
 
 
 
 
 
 
79
  },
80
  {
81
- "id": "case5",
82
- "npc_id": "young_apprentice_mage",
83
- "npc_location": "mage_tower_library",
84
- "description": "λ§ˆλ²•μ‚¬ κ²¬μŠ΅μƒκ³Ό κ³ λŒ€ μ£Όλ¬Έμ„œμ— λŒ€ν•΄ λŒ€ν™”ν•˜λŠ” μž₯λ©΄",
85
- "player_utterance": "이 μ£Όλ¬Έμ„œμ— 적힌 λ¬Έμž₯을 해석할 수 μžˆλ‚˜μš”?",
86
- "tags": {
87
- "quest_stage": "research",
88
- "relationship": 0.6,
89
- "trust": 0.7,
90
- "npc_mood": "excited",
91
- "player_reputation": "scholar",
92
- "style": "inquisitive"
93
- },
94
- "lore": "λ§ˆλ²•μ‚¬ νƒ‘μ˜ λ„μ„œκ΄€μ—λŠ” 수백 λ…„ 된 κ³ μ„œλ“€μ΄ λ³΄κ΄€λ˜μ–΄ μžˆλ‹€.",
95
- "context": [
96
- {"role": "player", "text": "이 책은 ꡉμž₯히 였래된 것 κ°™μ•„μš”."},
97
- {"role": "npc", "text": "λ§žμ•„μš”! 이런 건 정말 λ“œλ¬Όμ£ ."}
98
- ]
 
 
 
 
 
 
 
99
  }
100
  ]
 
1
  [
2
  {
3
+ "instruction": "ν”Œλ ˆμ΄μ–΄ λ°œν™”λ₯Ό λ°”νƒ•μœΌλ‘œ NPC의 λ‹€μŒ λŒ€μ‚¬μ™€ μƒνƒœ λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜μ„Έμš”.",
4
+ "input": {
5
+ "npc_id": "mother_abandoned_factory",
6
+ "npc_location": "map1",
7
+ "tags": {
8
+ "quest_stage": "in_progress",
9
+ "relationship": 0.35,
10
+ "trust": 0.35,
11
+ "npc_mood": "grief",
12
+ "player_reputation": "helpful",
13
+ "style": "emotional"
14
+ },
15
+ "lore": "",
16
+ "description": "",
17
+ "player_state": {
18
+ "items": ["forgotten_picture", "Jason's ID card"],
19
+ "actions": [],
20
+ "position": ""
21
+ },
22
+ "context": [
23
+ {
24
+ "role": "player",
25
+ "text": "사싀 이 곡μž₯을 λŒμ•„λ‹€λ‹ˆλ©΄μ„œ Jason의 흔적을 λ°œκ²¬ν–ˆμ–΄μš” 근데 ID μΉ΄λ“œμ˜ 사진을 λ³΄λ‹ˆ 제 지갑에 μžˆλŠ” 사진에 μžˆλŠ” 얼꡴인 κ±Έ 보고 Jasonκ³Ό μ €λŠ” μ•„λŠ” μ‚¬μ΄μ˜€λ˜ κ±Έ μ•Œκ²Œ λ˜μ—ˆμ£ . ν•˜μ§€λ§Œ 제 기얡은 λŒ€λΆ€λΆ„ μ‚¬λΌμ‘Œκ³  λͺ‡λ‹¬ μ „ νŒŒν‹°λ₯Ό ν–ˆλ˜ κΈ°μ–΅λ§Œ 흐리게 λ‚¨μ•„μžˆμ–΄μš”"
26
+ },
27
+ {
28
+ "role": "npc",
29
+ "text": "ν˜Ήμ‹œ κ·Έ νŒŒν‹°μ— Jason도 μžˆμ—ˆλ‚˜μš”?"
30
+ }
31
+ ],
32
+ "player_utterance": "μ•„!! 머리가!! κ°‘μžκΈ° κΈ°μ–΅λ‚¬μ–΄μš”! 이 μ§€κ°‘μ†μ˜ 사진이 κ·Έλ•Œ νŒŒν‹°μ—μ„œ μ°μ—ˆλ˜ μ‚¬μ§„μ΄μ—μš” Jason도 그곳에 μžˆμ—ˆλ„€μš”."
33
+ }
34
  },
35
  {
36
+ "instruction": "ν”Œλ ˆμ΄μ–΄ λ°œν™”λ₯Ό λ°”νƒ•μœΌλ‘œ NPC의 λ‹€μŒ λŒ€μ‚¬μ™€ μƒνƒœ λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜μ„Έμš”.",
37
+ "input": {
38
+ "npc_id": "blacksmith_village_center",
39
+ "npc_location": "village_square",
40
+ "tags": {
41
+ "quest_stage": "not_started",
42
+ "relationship": 0.0,
43
+ "trust": 0.0,
44
+ "npc_mood": "neutral",
45
+ "player_reputation": "unknown",
46
+ "style": "direct"
47
+ },
48
+ "lore": "",
49
+ "description": "",
50
+ "player_state": {
51
+ "items": ["broken sward"],
52
+ "actions": [],
53
+ "position": ""
54
+ },
55
+ "context": [
56
+ {"role": "player", "text": "μ €κΈ°.."},
57
+ {"role": "npc", "text": "였, μ—¬ν–‰μžκ΅°. 무슨 일둜 μ™”λ‚˜?"}
58
+ ],
59
+ "player_utterance": "이 검을 λ‹€μ‹œ μ“Έ 수 있게 고쳐쀄 수 μžˆλ‚˜μš”?"
60
+ }
61
  },
62
  {
63
+ "instruction": "ν”Œλ ˆμ΄μ–΄ λ°œν™”λ₯Ό λ°”νƒ•μœΌλ‘œ NPC의 λ‹€μŒ λŒ€μ‚¬μ™€ μƒνƒœ λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜μ„Έμš”.",
64
+ "input": {
65
+ "npc_id": "forest_hermit",
66
+ "npc_location": "deep_forest",
67
+ "tags": {
68
+ "quest_stage": "not_started",
69
+ "relationship": 0.0,
70
+ "trust": 0.0,
71
+ "npc_mood": "curious",
72
+ "player_reputation": "friendly",
73
+ "style": "polite"
74
+ },
75
+ "lore": "",
76
+ "description": "",
77
+ "player_state": {
78
+ "items": [],
79
+ "actions": [],
80
+ "position": ""
81
+ },
82
+ "context": [
83
+ {"role": "player", "text": "μ•ˆλ…•ν•˜μ„Έμš”, ν˜Ήμ‹œ μž μ‹œ 이야기 λ‚˜λˆŒ 수 μžˆμ„κΉŒμš”?"},
84
+ {"role": "npc", "text": "μ—¬κΈ°κΉŒμ§€ μ°Ύμ•„μ˜€λŠ” μ‚¬λžŒμ€ λ“œλ¬Όμ§€μš”.. 무슨 μš©κ±΄μ΄μ‹œμ£ ? μ—¬ν–‰μžμ—¬.."}
85
+ ],
86
+ "player_utterance": "ν˜Ήμ‹œ 이 κ·Όμ²˜μ—μ„œ ν‘Έλ₯ΈλΉ› μ•½μ΄ˆλ₯Ό 보신 적 μžˆλ‚˜μš”?"
87
+ }
88
  },
89
  {
90
+ "instruction": "ν”Œλ ˆμ΄μ–΄ λ°œν™”λ₯Ό λ°”νƒ•μœΌλ‘œ NPC의 λ‹€μŒ λŒ€μ‚¬μ™€ μƒνƒœ λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜μ„Έμš”.",
91
+ "input": {
92
+ "npc_id": "captain_port_authority",
93
+ "npc_location": "harbor",
94
+ "tags": {
95
+ "quest_stage": "in_progress",
96
+ "relationship": 0.0,
97
+ "trust": 0.0,
98
+ "npc_mood": "suspicious",
99
+ "player_reputation": "neutral",
100
+ "style": "persuasive"
101
+ },
102
+ "lore": "",
103
+ "description": "",
104
+ "player_state": {
105
+ "items": [],
106
+ "actions": [],
107
+ "position": ""
108
+ },
109
+ "context": [
110
+ {"role": "player", "text": "제 λ°°λ₯Ό 였늘 κΌ­ μΆœν•­μ‹œν‚€κ³  μ‹ΆμŠ΅λ‹ˆλ‹€"},
111
+ {"role": "npc", "text": "μ–΄λ ΅λ‹€. μ›μΉ™μ μœΌλ‘œ λ‹ΉμΌμ˜ μΆœν•­μΌμ •μ€ κ·Έ 전날에 κ²°μ •ν•œλ‹€ μ„œλ₯˜λŠ” λ‹€ μ€€λΉ„λλ‚˜? 그럼 내일 일정을 μž‘μ•„μ£Όμ§€"}
112
+ ],
113
+ "player_utterance": "무슨 일이 μžˆμ–΄λ„ 이 λ°°λ₯Ό 였늘 μ•ˆμ— κΌ­ μΆœν•­μ‹œμΌœμ•Όλ§Œ ν•©λ‹ˆλ‹€. 제발 ν—ˆκ°€λ₯Ό λΆ€νƒλ“œλ¦½λ‹ˆλ‹€."
114
+ }
115
  },
116
  {
117
+ "instruction": "ν”Œλ ˆμ΄μ–΄ λ°œν™”λ₯Ό λ°”νƒ•μœΌλ‘œ NPC의 λ‹€μŒ λŒ€μ‚¬μ™€ μƒνƒœ λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜μ„Έμš”.",
118
+ "input": {
119
+ "npc_id": "young_apprentice_mage",
120
+ "npc_location": "mage_tower_library",
121
+ "tags": {
122
+ "quest_stage": "in_progress",
123
+ "relationship": 0.3,
124
+ "trust": 0.35,
125
+ "npc_mood": "excited",
126
+ "player_reputation": "scholar",
127
+ "style": "inquisitive"
128
+ },
129
+ "lore": "",
130
+ "description": "",
131
+ "player_state": {
132
+ "items": ["magic spell scroll"],
133
+ "actions": [],
134
+ "position": ""
135
+ },
136
+ "context": [
137
+ {"role": "player", "text": "이 책은 ꡉμž₯히 였래된 것 κ°™μ•„μš”.."},
138
+ {"role": "npc", "text": "λ§žμ•„μš”! 이 책은 κ³ λŒ€λ‘œλΆ€ν„° μ „ν•΄μ§„λ‹€κ³  μ•Œλ €μ§„ 책이죠"}
139
+ ],
140
+ "player_utterance": "그럼 ν˜Ήμ‹œ 이 μ£Όλ¬Έμ„œμ— 적힌 λ¬Έμž₯을 해석할 수 μžˆλ‚˜μš”?"
141
+ }
142
  }
143
  ]
webtest_prompt.py CHANGED
@@ -4,12 +4,11 @@ def build_webtest_prompt(npc_id: str, npc_location: str, player_utt: str) -> str
4
  """
5
  Web Test μ „μš©: μ΅œμ†Œ μž…λ ₯κ°’(NPC ID, Location, Player λ°œν™”)으둜
6
  λͺ¨λΈ ν•™μŠ΅ 포맷에 λ§žλŠ” prompt λ¬Έμžμ—΄μ„ 생성.
7
- μ‹€μ œ API/κ²Œμž„ μ„œλΉ„μŠ€ κ²½λ‘œμ—μ„œλŠ” μ‚¬μš©ν•˜μ§€ μ•ŠμŒ.
8
  """
9
  pre = {
 
 
10
  "tags": {
11
- "npc_id": npc_id,
12
- "location": npc_location,
13
  "quest_stage": "",
14
  "relationship": "",
15
  "trust": "",
@@ -17,7 +16,11 @@ def build_webtest_prompt(npc_id: str, npc_location: str, player_utt: str) -> str
17
  "player_reputation": "",
18
  "style": ""
19
  },
20
- "player_state": {},
 
 
 
 
21
  "rag_main_docs": [],
22
  "context": [],
23
  "player_utterance": player_utt
@@ -43,7 +46,6 @@ def _assemble_prompt_for_model(pre: Dict[str, Any]) -> str:
43
  elif "DESCRIPTION:" in doc:
44
  desc_text += doc + "\n"
45
  else:
46
- # fallback: type 기반 뢄리 κ°€λŠ₯
47
  if "lore" in doc.lower():
48
  lore_text += doc + "\n"
49
  elif "description" in doc.lower():
@@ -51,8 +53,8 @@ def _assemble_prompt_for_model(pre: Dict[str, Any]) -> str:
51
 
52
  prompt = [
53
  "<SYS>",
54
- f"NPC_ID={tags.get('npc_id','')}",
55
- f"NPC_LOCATION={tags.get('location','')}",
56
  "TAGS:",
57
  f" quest_stage={tags.get('quest_stage','')}",
58
  f" relationship={tags.get('relationship','')}",
@@ -65,18 +67,14 @@ def _assemble_prompt_for_model(pre: Dict[str, Any]) -> str:
65
  f"LORE: {lore_text.strip() or '(μ—†μŒ)'}",
66
  f"DESCRIPTION: {desc_text.strip() or '(μ—†μŒ)'}",
67
  "</RAG>",
68
- "<PLAYER_STATE>"
 
 
 
 
 
69
  ]
70
 
71
- if ps.get("items"):
72
- prompt.append(f"items={','.join(ps['items'])}")
73
- if ps.get("actions"):
74
- prompt.append(f"actions={','.join(ps['actions'])}")
75
- if ps.get("position"):
76
- prompt.append(f"position={ps['position']}")
77
- prompt.append("</PLAYER_STATE>")
78
-
79
- prompt.append("<CTX>")
80
  for h in pre.get("context", []):
81
  prompt.append(f"{h['role']}: {h['text']}")
82
  prompt.append("</CTX>")
@@ -85,4 +83,4 @@ def _assemble_prompt_for_model(pre: Dict[str, Any]) -> str:
85
  prompt.append("<STATE>")
86
  prompt.append("<NPC>")
87
 
88
- return "\n".join(prompt)
 
4
  """
5
  Web Test μ „μš©: μ΅œμ†Œ μž…λ ₯κ°’(NPC ID, Location, Player λ°œν™”)으둜
6
  λͺ¨λΈ ν•™μŠ΅ 포맷에 λ§žλŠ” prompt λ¬Έμžμ—΄μ„ 생성.
 
7
  """
8
  pre = {
9
+ "npc_id": npc_id,
10
+ "npc_location": npc_location,
11
  "tags": {
 
 
12
  "quest_stage": "",
13
  "relationship": "",
14
  "trust": "",
 
16
  "player_reputation": "",
17
  "style": ""
18
  },
19
+ "player_state": {
20
+ "items": [],
21
+ "actions": [],
22
+ "position": ""
23
+ },
24
  "rag_main_docs": [],
25
  "context": [],
26
  "player_utterance": player_utt
 
46
  elif "DESCRIPTION:" in doc:
47
  desc_text += doc + "\n"
48
  else:
 
49
  if "lore" in doc.lower():
50
  lore_text += doc + "\n"
51
  elif "description" in doc.lower():
 
53
 
54
  prompt = [
55
  "<SYS>",
56
+ f"NPC_ID={pre.get('npc_id','')}",
57
+ f"NPC_LOCATION={pre.get('npc_location','')}",
58
  "TAGS:",
59
  f" quest_stage={tags.get('quest_stage','')}",
60
  f" relationship={tags.get('relationship','')}",
 
67
  f"LORE: {lore_text.strip() or '(μ—†μŒ)'}",
68
  f"DESCRIPTION: {desc_text.strip() or '(μ—†μŒ)'}",
69
  "</RAG>",
70
+ "<PLAYER_STATE>",
71
+ f"items={','.join(ps.get('items', []))}" if ps.get("items") else "items=",
72
+ f"actions={','.join(ps.get('actions', []))}" if ps.get("actions") else "actions=",
73
+ f"position={ps.get('position','')}",
74
+ "</PLAYER_STATE>",
75
+ "<CTX>"
76
  ]
77
 
 
 
 
 
 
 
 
 
 
78
  for h in pre.get("context", []):
79
  prompt.append(f"{h['role']}: {h['text']}")
80
  prompt.append("</CTX>")
 
83
  prompt.append("<STATE>")
84
  prompt.append("<NPC>")
85
 
86
+ return "\n".join(prompt)