alienet commited on
Commit
e636070
·
1 Parent(s): 81c1391

first commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. .gitignore +9 -0
  3. BookWorld.py +1064 -0
  4. Dockerfile +20 -0
  5. LICENSE +201 -0
  6. app.py +240 -0
  7. bw_utils.py +466 -0
  8. config.json +20 -0
  9. convert_sillytavern_cards_to_data.py +31 -0
  10. data/locations/A_Song_of_Ice_and_Fire.json +45 -0
  11. data/locations/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.json +9 -0
  12. data/locations/example_locations.json +24 -0
  13. data/maps/A_Song_of_Ice_and_Fire.csv +5 -0
  14. data/maps/A_Song_of_Ice_and_Fire_bloody_wedding.csv +2 -0
  15. data/maps/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.csv +2 -0
  16. data/maps/example_map.csv +4 -0
  17. data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/icon.png +3 -0
  18. data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/role_info.json +27 -0
  19. data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/icon.png +3 -0
  20. data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/role_info.json +28 -0
  21. data/roles/A_Song_of_Ice_and_Fire/Catelyn-Stark-zh/role_info.json +35 -0
  22. data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/icon.png +3 -0
  23. data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/role_info.json +28 -0
  24. data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/icon.png +3 -0
  25. data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/role_info.json +28 -0
  26. data/roles/A_Song_of_Ice_and_Fire/Edmure-Tully-zh/role_info.json +34 -0
  27. data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/icon.png +3 -0
  28. data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/role_info.json +29 -0
  29. data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/icon.png +3 -0
  30. data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/role_info.json +28 -0
  31. data/roles/A_Song_of_Ice_and_Fire/Robb-Stark-zh/role_info.json +37 -0
  32. data/roles/A_Song_of_Ice_and_Fire/RobbStark-zh/role_info.json +37 -0
  33. data/roles/A_Song_of_Ice_and_Fire/Roose-Bolton-zh/role_info.json +36 -0
  34. data/roles/A_Song_of_Ice_and_Fire/Roslin-Frey-zh/role_info.json +34 -0
  35. data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/icon.png +3 -0
  36. data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/role_info.json +28 -0
  37. data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/icon.png +3 -0
  38. data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/role_info.json +28 -0
  39. data/roles/A_Song_of_Ice_and_Fire/Tywin-Lannister-zh/role_info.json +35 -0
  40. data/roles/A_Song_of_Ice_and_Fire/Walder-Frey-zh/role_info.json +35 -0
  41. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_info.json +9 -0
  42. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_lines.jsonl +102 -0
  43. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_info.json +9 -0
  44. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_lines.jsonl +6 -0
  45. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Cheshire_Cat-en/role_info.json +9 -0
  46. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_info.json +9 -0
  47. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_lines.jsonl +4 -0
  48. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_info.json +9 -0
  49. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_lines.jsonl +1 -0
  50. data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Duchess-en/role_info.json +9 -0
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.webp filter=lfs diff=lfs merge=lfs -text
39
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
40
+ *.ico filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ **/__pycache__/**
2
+ **/chromadb_saves/
3
+ /log/
4
+ /experiment_saves/
5
+ run.sh
6
+ /config/
7
+ .vscode/
8
+ nohup.out
9
+ BookWorld_test.py
BookWorld.py ADDED
@@ -0,0 +1,1064 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from tqdm import tqdm
3
+ import json
4
+ import os
5
+ import warnings
6
+ import random
7
+ from typing import Any, Dict, List, Optional, Literal
8
+ from collections import defaultdict
9
+ import uuid
10
+
11
+ from bw_utils import *
12
+ from modules.main_role_agent import RPAgent
13
+ from modules.world_agent import WorldAgent
14
+ from modules.history_manager import HistoryManager
15
+ import argparse
16
+
17
+ warnings.filterwarnings('ignore')
18
+
19
+ class Server():
20
+ def __init__(self,
21
+ preset_path: str,
22
+ world_llm_name: str,
23
+ role_llm_name: str,
24
+ embedding_name:str = "bge-m3") :
25
+ """
26
+ The initialization function of the system.
27
+
28
+ Args:
29
+ preset_path (str): The path to config file of this experiment.
30
+ world_llm_name (str, optional): The base model of the world agent. Defaults to 'gpt-4o'.
31
+ role_llm_name (str, optional): The base model of all the role agents. Defaults to 'gpt-4o'.
32
+ mode (str, optional): If set to be 'script', the role agents will act according to the given script.
33
+ If set to be 'free', the role agents will act freely based on their backround.
34
+ Defaults to 'free'.
35
+ """
36
+
37
+ self.role_llm_name: str = role_llm_name
38
+ self.world_llm_name: str = world_llm_name
39
+ self.embedding_name:str = embedding_name
40
+ config = load_json_file(preset_path)
41
+ self.preset_path = preset_path
42
+ self.config: Dict = config
43
+ self.experiment_name: str = os.path.basename(preset_path).replace(".json","") + "/" + config["experiment_subname"] + "_" + role_llm_name
44
+
45
+ role_agent_codes: List[str] = config['role_agent_codes']
46
+ world_file_path: str = config["world_file_path"]
47
+ map_file_path: str = config["map_file_path"] if "map_file_path" in config else ""
48
+ role_file_dir: str = config["role_file_dir"] if "role_file_dir" in config else "./data/roles/"
49
+ loc_file_path: str = config["loc_file_path"]
50
+ self.intervention: str = "" if "intervention" in config else ""
51
+ self.event = self.intervention
52
+ self.script: str = config["script"] if "script" in config else ""
53
+ self.language: str = config["language"] if "language" in config else "zh"
54
+ self.source:str = config["source"] if "source" in config else ""
55
+
56
+ self.idx: int = 0
57
+ self.cur_round: int = 0
58
+ self.progress: str = "剧本刚刚开始,还什么都没有发生" if self.language == 'zh' else "The story has just begun, nothing happens yet."
59
+ self.moving_roles_info: Dict[str, Any] = {}
60
+ self.history_manager = HistoryManager()
61
+ self.start_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
62
+ self.current_status = {
63
+ "location_code":"",
64
+ "group":role_agent_codes,
65
+ }
66
+ self.scene_characters = {}
67
+ self.event_history = []
68
+
69
+ self.role_llm = get_models(role_llm_name)
70
+ self.logger = get_logger(self.experiment_name)
71
+ self.init_role_agents(role_agent_codes = role_agent_codes,
72
+ role_file_dir = role_file_dir,
73
+ world_file_path=world_file_path,
74
+ llm = self.role_llm)
75
+
76
+ if world_llm_name == role_llm_name:
77
+ self.world_llm = self.role_llm
78
+ else:
79
+ self.world_llm = get_models(world_llm_name)
80
+ self.init_world_agent_from_file(world_file_path = world_file_path,
81
+ map_file_path = map_file_path,
82
+ loc_file_path = loc_file_path,
83
+ llm = self.world_llm)
84
+
85
+ # Init
86
+ def init_role_agents(self,
87
+ role_agent_codes: List[str],
88
+ role_file_dir:str,
89
+ world_file_path:str,
90
+ llm) -> None:
91
+ self.role_codes: List[str] = role_agent_codes
92
+ self.role_agents: Dict[str, RPAgent] = {}
93
+
94
+ for role_code in role_agent_codes:
95
+ if check_role_code_availability(role_code,role_file_dir):
96
+ self.role_agents[role_code] = RPAgent(role_code=role_code,
97
+ role_file_dir=role_file_dir,
98
+ world_file_path=world_file_path,
99
+ source = self.source,
100
+ language=self.language,
101
+ llm_name = self.role_llm_name,
102
+ llm = llm,
103
+ embedding_name=self.embedding_name,
104
+ )
105
+ # print(f"{role_code} Initialized.")
106
+ else:
107
+ print(f"Warning: The specified role `{role_code}` does not exist.")
108
+
109
+ def init_world_agent_from_file(self,
110
+ world_file_path: str,
111
+ map_file_path: str,
112
+ loc_file_path: str,
113
+ llm) -> None:
114
+ self.world_agent: WorldAgent = WorldAgent(world_file_path = world_file_path,
115
+ location_file_path = loc_file_path,
116
+ map_file_path = map_file_path,
117
+ llm_name=self.world_llm_name,
118
+ llm = llm,
119
+ language=self.language)
120
+ for role_code in self.role_agents:
121
+ self.role_agents[role_code].world_db = self.world_agent.db
122
+ self.role_agents[role_code].world_db_name = self.world_agent.db_name
123
+
124
+ def init_role_locations(self, random_allocate: bool = True):
125
+ """
126
+ Set initial positions of the roles.
127
+
128
+ Args:
129
+ random_allocate (bool, optional): if set to be True, the initial positions of the roles are randomly assigned. Defaults to True.
130
+ """
131
+ init_locations_code = random.choices(self.world_agent.locations, k = len(self.role_codes))
132
+ for i,role_code in enumerate(self.role_codes):
133
+ self.role_agents[role_code].set_location(init_locations_code[i], self.world_agent.find_location_name(init_locations_code[i]))
134
+ info_text = f"{self.role_agents[role_code].nickname} 现在位于 {self.world_agent.find_location_name(init_locations_code[i])}" \
135
+ if self.language == "zh" else f"{self.role_agents[role_code].nickname} is now located at {self.world_agent.find_location_name(init_locations_code[i])}"
136
+ self.log(info_text)
137
+
138
+ # Simulation
139
+ def simulate_generator(self,
140
+ rounds: int = 10,
141
+ save_dir: str = "",
142
+ if_save: Literal[0,1] = 0,
143
+ mode: Literal["free", "script"] = "free",
144
+ scene_mode: Literal[0,1] = 1,):
145
+ """
146
+ The main function of the simulation.
147
+
148
+ Args:
149
+ rounds (int, optional): The max rounds of simulation. Defaults to 10.
150
+ save_dir (str, optional): _description_. Defaults to "".
151
+ if_save (Literal[0,1], optional): _description_. Defaults to 0.
152
+ """
153
+ self.mode = mode
154
+ meta_info: Dict[str, Any] = self.continue_simulation_from_file(save_dir)
155
+ self.if_save: int = if_save
156
+ start_round: int = meta_info["round"]
157
+ sub_start_round:int = meta_info["sub_round"] if "sub_round" in meta_info else 0
158
+ if start_round == rounds: return
159
+
160
+ # Setting Locations
161
+ if not meta_info["location_setted"]:
162
+ self.log("========== Start Location Setting ==========")
163
+ self.init_role_locations()
164
+ self._save_current_simulation("location")
165
+
166
+ # Setting Goals
167
+ if not meta_info["goal_setted"]:
168
+ yield ("system","","-- Setting Goals --",None)
169
+ self.log("========== Start Goal Setting ==========")
170
+
171
+ if self.mode == "free":
172
+ self.get_event()
173
+ self.log(f"--------- Free Mode: Current Event ---------\n{self.event}\n")
174
+ self.event_history.append(self.event)
175
+ elif self.mode == "script":
176
+ self.get_script()
177
+ self.log(f"--------- Script Mode: Setted Script ---------\n{self.script}\n")
178
+ self.event_history.append(self.event)
179
+ if self.mode == "free":
180
+ for role_code in self.role_codes:
181
+ motivation = self.role_agents[role_code].set_motivation(
182
+ world_description = self.world_agent.description,
183
+ other_roles_info = self._get_group_members_info_dict(self.role_agents),
184
+ intervention = self.event,
185
+ script = self.script
186
+ )
187
+ info_text = f"{self.role_agents[role_code].nickname} 设立了动机: {motivation}" \
188
+ if self.language == "zh" else f"{self.role_agents[role_code].nickname} has set the motivation: {motivation}"
189
+
190
+ record_id = str(uuid.uuid4())
191
+ self.log(info_text)
192
+ self.record(role_code=role_code,
193
+ detail=info_text,
194
+ actor = role_code,
195
+ group = [role_code],
196
+ actor_type = 'role',
197
+ act_type="goal setting",
198
+ record_id = record_id)
199
+ yield ("role",role_code,info_text,record_id)
200
+
201
+ self._save_current_simulation("goal")
202
+
203
+ yield ("system","","-- Simulation Started --",None)
204
+ selected_role_codes = []
205
+ # Simulating
206
+ for current_round in range(start_round, rounds):
207
+ self.cur_round = current_round
208
+ self.log(f"========== Round {current_round+1} Started ==========")
209
+ if self.event and current_round >= 1:
210
+ self.log(f"--------- Current Event ---------\n{self.event}\n")
211
+ yield ("world","","-- Current Event --\n"+self.event, None)
212
+ self.event_history.append(self.event)
213
+
214
+ if len(self.moving_roles_info) == len(self.role_codes):
215
+ self.settle_movement()
216
+ continue
217
+
218
+ # Characters in next scene
219
+ if scene_mode:
220
+ group = self._name2code(
221
+ self.world_agent.decide_screen_actors(
222
+ self._get_locations_info(False),
223
+ self.history_manager.get_recent_history(5),
224
+ self.event,
225
+ list(set(selected_role_codes + list(self.moving_roles_info.keys())))))
226
+
227
+ selected_role_codes += group
228
+ if len(selected_role_codes) > len(self.role_codes):
229
+ selected_role_codes = []
230
+ else:
231
+ group = self.role_codes
232
+ self.current_status['group'] = group
233
+ self.current_status['location_code'] = self.role_agents[group[0]].location_code
234
+ self.scene_characters[str(current_round)] = group
235
+
236
+ # Prologue
237
+ # if current_round == 0 and len(group) > 0
238
+ # prologue = self.world_agent.generate_location_prologue(location_code=self.role_agents[group[0]].location_code, history_text=self._get_history_text(group),event=self.event,location_info_text=self._find_roles_at_location(self.role_agents[group[0]].location_code,name=True))
239
+ # self.log("--Prologue--: "+prologue)
240
+ # self.record(role_code="None",detail=prologue,act_type="prologue")
241
+ start_idx = len(self.history_manager)
242
+
243
+ sub_round = sub_start_round
244
+ for sub_round in range(sub_start_round,3):
245
+ if self.mode == "script":
246
+ self.script_instruct(self.progress)
247
+ else:
248
+ for role_code in group:
249
+ self.role_agents[role_code].update_goal(other_roles_status=self._get_status_text(self.role_codes))
250
+
251
+ for role_code in group:
252
+ if scene_mode:
253
+ role_code = self._name2code(self.world_agent.decide_next_actor("\n".join(self.history_manager.get_recent_history(3)),self._get_group_members_info_text(group,status=True),self.script)) if scene_mode else role_code
254
+
255
+ yield from self.implement_next_plan(role_code = role_code,
256
+ group = group)
257
+ self._save_current_simulation("action", current_round, sub_round)
258
+
259
+ if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_recent_history(len(self.history_manager)-start_idx)))
260
+ if if_end:
261
+ record_id = str(uuid.uuid4())
262
+ self.log("--Epilogue--: "+epilogue)
263
+ self.record(role_code = "None",
264
+ detail = epilogue,
265
+ actor_type="world",
266
+ act_type="epilogue",
267
+ actor = "world",
268
+ group = [],
269
+ record_id = record_id)
270
+ yield ("world","","--Epilogue--: "+epilogue, record_id)
271
+
272
+ break
273
+
274
+
275
+ for role_code in group:
276
+ yield from self.decide_whether_to_move(role_code = role_code,
277
+ group = self._find_group(role_code))
278
+ self.role_agents[role_code].update_status()
279
+
280
+ self.settle_movement()
281
+ self.update_event(group)
282
+
283
+ sub_start_round = 0
284
+ self._save_current_simulation("action", current_round + 1,sub_round + 1)
285
+
286
+ # Main functions using llm
287
+ def implement_next_plan(self,role_code: str, group: List[str]):
288
+ other_roles_info = self._get_group_members_info_dict(group)
289
+ plan = self.role_agents[role_code].plan(
290
+ other_roles_info = other_roles_info,
291
+ available_locations = self.world_agent.locations,
292
+ world_description = self.world_agent.description,
293
+ intervention = self.event,
294
+ )
295
+
296
+ info_text = plan["detail"]
297
+ if plan["target_role_codes"]:
298
+ plan["target_role_codes"] = self._name2code(plan["target_role_codes"])
299
+
300
+
301
+ record_id = str(uuid.uuid4())
302
+ self.log(f"-Action-\n{self.role_agents[role_code].role_name}: "+ info_text)
303
+ self.record(role_code = role_code,
304
+ detail = plan["detail"],
305
+ actor_type = 'role',
306
+ act_type = "plan",
307
+ actor = role_code,
308
+ group = plan["target_role_codes"] + [role_code],
309
+ plan = plan,
310
+ record_id = record_id
311
+ )
312
+ yield ("role", role_code, info_text, record_id)
313
+
314
+ if plan["interact_type"] == "single" and len(plan["target_role_codes"]) == 1 and plan["target_role_codes"][0] in group:
315
+ yield from self.start_single_role_interaction(plan, record_id)
316
+ elif plan["interact_type"] == "multi" and len(plan["target_role_codes"]) > 1 and set(plan["target_role_codes"]).issubset(set(group)) :
317
+ yield from self.start_multi_role_interaction(plan, record_id)
318
+ elif plan["interact_type"] == "enviroment":
319
+ yield from self.start_enviroment_interaction(plan,role_code, record_id)
320
+ elif plan["interact_type"] == "npc" and plan["target_npc_name"]:
321
+ yield from self.start_npc_interaction(plan,role_code,target_name=plan["target_npc_name"], record_id = record_id)
322
+ return info_text
323
+
324
+ def decide_whether_to_move(self,
325
+ role_code: str,
326
+ group: List[str]):
327
+ if len(self.world_agent.locations) <= 1:
328
+ return False
329
+ if_move, move_detail, destination_code = self.role_agents[role_code].move(locations_info_text = self._get_locations_info(),
330
+ locations_info = self.world_agent.locations_info)
331
+ if if_move:
332
+ self.log(move_detail)
333
+ print(f"角色选择移动。{self.role_agents[role_code].role_name}正在前往{self.world_agent.find_location_name(destination_code)}" if self.language == "zh" else f"The role decides to move. {self.role_agents[role_code].role_name} is heading to {self.world_agent.find_location_name(destination_code)}.")
334
+ self.record(role_code = role_code,
335
+ detail = move_detail,
336
+ actor_type = 'role',
337
+ act_type = "move",
338
+ actor = role_code,
339
+ group = [role_code],
340
+ destinatiion_code = destination_code
341
+ )
342
+ yield ("role",role_code,move_detail,None)
343
+
344
+ distance = self.world_agent.get_distance(self.role_agents[role_code].location_code, destination_code)
345
+ self.role_agents[role_code].set_location(location_code=None, location_name=None)
346
+ self.moving_roles_info[role_code] = {
347
+ "location_code":destination_code,
348
+ "distance":distance
349
+ }
350
+ return if_move
351
+
352
+ def start_enviroment_interaction(self,
353
+ plan: Dict[str, Any],
354
+ role_code: str,
355
+ record_id: str):
356
+ """
357
+ Handles the role's interaction with the environment.
358
+ It gets interaction results from agents in the world, record the result and update the status of the role.
359
+
360
+ Args:
361
+ plan (Dict[str, Any]): The details of the action.
362
+ role_code (str): The action maker.
363
+
364
+ Returns:
365
+ (str): The enviroment response.
366
+ """
367
+ if "action" not in plan:
368
+ plan["action"] = ""
369
+ self.current_status['group'] = [role_code]
370
+ location_code = self.role_agents[role_code].location_code
371
+ result = self.world_agent.enviroment_interact(action_maker_name = self.role_agents[role_code].role_name,
372
+ action = plan["action"],
373
+ action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
374
+ location_code = location_code)
375
+ env_record_id = str(uuid.uuid4())
376
+ self.log(f"(Enviroment):{result}")
377
+ self.record(role_code = role_code,
378
+ detail = result,
379
+ actor_type = 'world',
380
+ act_type = "enviroment",
381
+ initiator = role_code,
382
+ actor = "world",
383
+ group = [role_code],
384
+ record_id = env_record_id)
385
+ yield ("world","","(Enviroment):" + result, env_record_id)
386
+
387
+ return conceal_thoughts(self.history_manager.search_record_detail(record_id)) + self.history_manager.search_record_detail(env_record_id)
388
+
389
+ def start_npc_interaction(self,
390
+ plan: Dict[str, Any],
391
+ role_code: str,
392
+ target_name: str,
393
+ record_id: str,
394
+ max_rounds: int = 3):
395
+ """
396
+ Handles the role's interaction with the environment.
397
+ It gets interaction results from agents in the world, record the result and update the status of the role.
398
+
399
+ Args:
400
+ plan (Dict[str, Any]): The details of the action.
401
+ role_code (str): The action maker.
402
+ target_name (str): The target npc.
403
+
404
+ Returns:
405
+ (str): The enviroment response.
406
+ """
407
+ interaction = plan
408
+ start_idx = len(self.history_manager)
409
+
410
+ self.log(f"----------NPC Interaction----------\n")
411
+ self.current_status['group'] = [role_code,target_name]
412
+ for round in range(max_rounds):
413
+ npc_interaction = self.world_agent.npc_interact(action_maker_name=self.role_agents[role_code].role_name,
414
+ action_detail=self.history_manager.search_record_detail(record_id),
415
+ location_name=self.role_agents[role_code].location_name,
416
+ target_name=target_name)
417
+ npc_detail = npc_interaction["detail"]
418
+
419
+ npc_record_id = str(uuid.uuid4())
420
+ self.log(f"{target_name}: " + npc_detail)
421
+ self.record(role_code = role_code,
422
+ detail = npc_detail,
423
+ actor_type = 'world',
424
+ act_type = "npc",
425
+ actor = "world",
426
+ group = [role_code],
427
+ npc_name = target_name,
428
+ record_id = npc_record_id
429
+ )
430
+ yield ("world","",f"(NPC-{target_name}):" + npc_detail, npc_record_id)
431
+
432
+ if npc_interaction["if_end_interaction"]:
433
+ break
434
+
435
+ interaction = self.role_agents[role_code].npc_interact(
436
+ npc_name = target_name,
437
+ npc_response = self.history_manager.search_record_detail(npc_record_id),
438
+ history = self.history_manager.get_subsequent_history(start_idx = start_idx),
439
+ intervention = self.event
440
+ )
441
+ detail = interaction["detail"]
442
+
443
+ record_id = str(uuid.uuid4())
444
+ self.log(f"{self.role_agents[role_code].role_name}: " + detail)
445
+ self.record(role_code = role_code,
446
+ detail = detail,
447
+ actor_type = 'role',
448
+ act_type = "npc",
449
+ actor = role_code,
450
+ group = [role_code],
451
+ npc_name = target_name,
452
+ record_id = record_id)
453
+ yield ("role",role_code,detail,record_id)
454
+
455
+ if interaction["if_end_interaction"]:
456
+ break
457
+ if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
458
+ if if_end:
459
+ break
460
+
461
+ return "\n".join(self.history_manager.get_subsequent_history(start_idx = start_idx))
462
+
463
+ def start_single_role_interaction(self,
464
+ plan: Dict[str, Any],
465
+ record_id: str,
466
+ max_rounds: int = 8):
467
+ interaction = plan
468
+ acted_role_code = interaction["role_code"]
469
+ acting_role_code = interaction["target_role_codes"][0]
470
+ if acting_role_code not in self.role_codes:
471
+ print(f"Warning: Role {acting_role_code} does not exist.")
472
+ return
473
+ self.current_status['group'] = [acted_role_code,acting_role_code]
474
+
475
+ start_idx = len(self.history_manager)
476
+ for round in range(max_rounds):
477
+ interaction = self.role_agents[acting_role_code].single_role_interact(
478
+ action_maker_code = acted_role_code,
479
+ action_maker_name = self.role_agents[acted_role_code].role_name,
480
+ action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
481
+ action_maker_profile = self.role_agents[acted_role_code].role_profile,
482
+ intervention = self.event
483
+ )
484
+
485
+ detail = interaction["detail"]
486
+
487
+ record_id = str(uuid.uuid4())
488
+ self.log(f"{self.role_agents[acting_role_code].role_name}: " + detail)
489
+ self.record(role_code = acting_role_code,
490
+ detail = detail,
491
+ actor_type = 'role',
492
+ act_type = "single",
493
+ group = [acted_role_code,acting_role_code],
494
+ target_role_code = acting_role_code,
495
+ planning_role_code = plan["role_code"],
496
+ round = round,
497
+ record_id = record_id
498
+ )
499
+ yield ("role",acting_role_code,detail,record_id)
500
+
501
+ if interaction["if_end_interaction"]:
502
+ return
503
+ if interaction["extra_interact_type"] == "npc":
504
+ print("---Extra NPC Interact---")
505
+ result = yield from self.start_npc_interaction(plan=interaction,
506
+ role_code=acted_role_code,
507
+ target_name=interaction["target_npc_name"],
508
+ record_id=record_id)
509
+ interaction["detail"] = result
510
+
511
+ elif interaction["extra_interact_type"] == "enviroment":
512
+ print("---Extra Env Interact---")
513
+ result = yield from self.start_enviroment_interaction(plan=interaction,role_code=acted_role_code,record_id=record_id)
514
+ interaction["detail"] = result
515
+
516
+ if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
517
+ if if_end:
518
+ break
519
+ acted_role_code,acting_role_code = acting_role_code,acted_role_code
520
+ return
521
+
522
+ def start_multi_role_interaction(self,
523
+ plan: Dict[str, Any],
524
+ record_id: str,
525
+ max_rounds: int = 8):
526
+
527
+ interaction = plan
528
+ acted_role_code = interaction["role_code"]
529
+ group = interaction["target_role_codes"]
530
+ group.append(acted_role_code)
531
+
532
+ for code in group:
533
+ if code not in self.role_codes:
534
+ print(f"Warning: Role {code} does not exist.")
535
+ return
536
+ self.current_status['group'] = group
537
+
538
+ start_idx = len(self.history_manager)
539
+ other_roles_info = self._get_group_members_info_dict(group)
540
+
541
+ for round in range(max_rounds):
542
+ acting_role_code = self._name2code(self.world_agent.decide_next_actor(history_text = "\n".join(self.history_manager.get_recent_history(3)),
543
+ roles_info_text = self._get_group_members_info_text(remove_list_elements(group,acted_role_code),status=True)))
544
+
545
+
546
+ interaction = self.role_agents[acting_role_code].multi_role_interact(
547
+ action_maker_code = acted_role_code,
548
+ action_maker_name = self.role_agents[acted_role_code].role_name,
549
+ action_detail = conceal_thoughts(self.history_manager.search_record_detail(record_id)),
550
+ action_maker_profile = self.role_agents[acted_role_code].role_profile,
551
+ other_roles_info = other_roles_info,
552
+ intervention = self.event
553
+ )
554
+
555
+ detail = interaction["detail"]
556
+
557
+ record_id = str(uuid.uuid4())
558
+ self.log(f"{self.role_agents[acting_role_code].role_name}: "+ detail)
559
+ self.record(role_code = acting_role_code,
560
+ detail = detail,
561
+ actor_type = 'role',
562
+ act_type = "multi",
563
+ group = group,
564
+ actor = acting_role_code,
565
+ planning_role_code = plan["role_code"],
566
+ round = round,
567
+ record_id = record_id
568
+ )
569
+ yield ("role",acting_role_code,detail,record_id)
570
+
571
+
572
+ if interaction["if_end_interaction"]:
573
+ break
574
+ result = ""
575
+ if interaction["extra_interact_type"] == "npc":
576
+ print("---Extra NPC Interact---")
577
+ result = yield from self.start_npc_interaction(plan=interaction,role_code=acting_role_code,target_name=interaction["target_npc_name"],record_id = record_id)
578
+ elif interaction["extra_interact_type"] == "enviroment":
579
+ print("---Extra Env Interact---")
580
+ result = yield from self.start_enviroment_interaction(plan=interaction,role_code=acting_role_code,record_id = record_id)
581
+ interaction["detail"] = self.history_manager.search_record_detail(record_id) + result
582
+ acted_role_code = acting_role_code
583
+ if_end,epilogue = self.world_agent.judge_if_ended("\n".join(self.history_manager.get_subsequent_history(start_idx)))
584
+ if if_end:
585
+ break
586
+
587
+ return
588
+
589
+ # Sub functions using llm
590
+ def script_instruct(self,
591
+ last_progress: str,
592
+ top_k: int = 5):
593
+ """
594
+ Under the script mode, generate instruction for the roles at the beginning of each round.
595
+
596
+ Args:
597
+ last_progress (str): Where the script went in the last round.
598
+ top_k (int, optional): The number of action history of each role to refer. Defaults to 1.
599
+
600
+ Returns:
601
+ Dict[str, Any]: Instruction for each role.
602
+ """
603
+ roles_info_text = self._get_group_members_info_text(self.role_codes,status=True)
604
+ history_text = "\n".join([self.role_agents[role_code].history_manager.get_recent_history(1)[0] for role_code in self.role_codes])
605
+
606
+ instruction = self.world_agent.get_script_instruction(
607
+ roles_info_text=roles_info_text,
608
+ event = self.event,
609
+ history_text=history_text,
610
+ script=self.script,
611
+ last_progress = last_progress)
612
+
613
+ for code in instruction:
614
+ if code == "progress":
615
+ self.log("剧本进度:"+ instruction["progress"]) if self.language == "zh" else self.log("Current Stage:"+ instruction["progress"])
616
+ elif code in self.role_codes:
617
+ # self.role_agents[code].update_goal(instruction = instruction[code])
618
+ self.role_agents[code].goal = instruction[code]
619
+ else:
620
+ print("Instruction failed, role code:",code)
621
+ return instruction
622
+
623
+ def get_event(self,):
624
+ if self.intervention == "" and not self.script:
625
+ roles_info_text = self._get_group_members_info_text(self.role_codes,profile=True)
626
+ status_text = self._get_status_text(self.role_codes)
627
+ event = self.world_agent.generate_event(roles_info_text=roles_info_text,event=self.intervention,history_text=status_text)
628
+ self.intervention = event
629
+ elif self.intervention == "" and self.script:
630
+ self.intervention = self.script
631
+ self.event = self.intervention
632
+ return self.intervention
633
+
634
+ def get_script(self,):
635
+ if self.script == "":
636
+ roles_info_text = self._get_group_members_info_text(self.role_codes,profile=True)
637
+ status = "\n".join([self.role_agents[role_code].status for role_code in self.role_codes])
638
+ script = self.world_agent.generate_script(roles_info_text=roles_info_text,event=self.intervention,history_text=status)
639
+ self.script = script
640
+ # self.event = self.script
641
+ return self.script
642
+
643
+ def update_event(self, group: List[str], top_k: int = 1):
644
+ if self.intervention == "":
645
+ self.event = ""
646
+ else:
647
+ status_text = self._get_status_text(group)
648
+ self.event = self.world_agent.update_event(self.event, self.intervention, status_text, script = self.script)
649
+
650
+ # other
651
+ def record(self,
652
+ role_code: str,
653
+ detail: str,
654
+ actor_type: str,
655
+ act_type: str,
656
+ group: List[str] = [],
657
+ actor: str = "",
658
+ record_id = None,
659
+ **kwargs):
660
+ if act_type == "plan" and "plan" in kwargs:
661
+ detail = f"{self.role_agents[role_code].nickname}: {detail}"
662
+ interact_type = kwargs["plan"]["interact_type"]
663
+ target = ", ".join(kwargs["plan"]["target_role_codes"])
664
+ other_info = f"Interact type: {interact_type}, Target: {target}"
665
+ elif act_type == "move" and "destination_code" in kwargs:
666
+ destination = kwargs["destination_code"]
667
+ other_info = f"Desitination:{destination}"
668
+ elif act_type == "single":
669
+ detail = f"{self.role_agents[role_code].nickname}: {detail}"
670
+ target, planning_role, round = kwargs["target_role_code"],kwargs["planning_role_code"],kwargs["round"]
671
+ other_info = f"Target: {target}, Planning Role: {planning_role}, Round: {round}"
672
+ elif act_type == "multi":
673
+ detail = f"{self.role_agents[role_code].nickname}: {detail}"
674
+ planning_role, round = kwargs["planning_role_code"],kwargs["round"]
675
+ other_info = f"Group member:{group}, Planning Role: {planning_role}, Round:{round},"
676
+ elif act_type == "npc":
677
+ name = kwargs["npc_name"]
678
+ other_info = f"Target: {name}"
679
+ elif act_type == "enviroment":
680
+ other_info = ""
681
+ else:
682
+ other_info = ""
683
+ record = {
684
+ "cur_round":self.cur_round,
685
+ "role_code":role_code,
686
+ "detail":detail,
687
+ "actor":actor,
688
+ "group":group, # visible group
689
+ "actor_type":actor_type,
690
+ "act_type":act_type,
691
+ "other_info":other_info,
692
+ "record_id":record_id
693
+ }
694
+ self.history_manager.add_record(record)
695
+ for code in group:
696
+ self.role_agents[code].record(record)
697
+
698
+ def settle_movement(self,):
699
+ for role_code in self.moving_roles_info.copy():
700
+ if not self.moving_roles_info[role_code]["distance"]:
701
+ location_code = self.moving_roles_info[role_code]["location_code"]
702
+ self.role_agents[role_code].set_location(location_code, self.world_agent.find_location_name(location_code))
703
+ self.log(f"{self.role_agents[role_code].role_name} 已到达 【{self.world_agent.find_location_name(location_code)}】" if self.language == "zh" else
704
+ f"{self.role_agents[role_code].role_name} has reached [{self.world_agent.find_location_name(location_code)}]")
705
+ del self.moving_roles_info[role_code]
706
+ else:
707
+ self.moving_roles_info[role_code]["distance"] -= 1
708
+
709
+ def _find_group(self,role_code):
710
+ return [code for code in self.role_codes if self.role_agents[code].location_code==self.role_agents[role_code].location_code]
711
+
712
+ def _find_roles_at_location(self,location_code,name = False):
713
+ if name:
714
+ return [self.role_agents[code].nickname for code in self.role_codes if self.role_agents[code].location_code==location_code]
715
+ else:
716
+ return [code for code in self.role_codes if self.role_agents[code].location_code==location_code]
717
+
718
+ def _get_status_text(self,group):
719
+ return "\n".join([self.role_agents[role_code].status for role_code in group])
720
+
721
+ def _get_group_members_info_text(self,group, profile = False,status = False):
722
+ roles_info_text = ""
723
+ for i, role_code in enumerate(group):
724
+ name = self.role_agents[role_code].role_name
725
+ roles_info_text += f"{i+1}. {name}\n(role_code:{role_code})\n"
726
+ if profile:
727
+ profile = self.role_agents[role_code].role_profile
728
+ roles_info_text += f"{profile}\n"
729
+ if status:
730
+ status = self.role_agents[role_code].status
731
+ roles_info_text += f"{status}\n"
732
+ return roles_info_text
733
+
734
+ def _get_group_members_info_dict(self,group: List[str]):
735
+ info = {
736
+ role_code: {
737
+ "nickname": self.role_agents[role_code].nickname,
738
+ "profile": self.role_agents[role_code].role_profile
739
+ }
740
+ for role_code in group
741
+ }
742
+ return info
743
+
744
+ def _get_locations_info(self,detailed = True):
745
+ location_info_text = "---当前各角色位置---\n" if self.language == "zh" else "---Current Location of Roles---\n"
746
+ if detailed:
747
+ for i,location_code in enumerate(self.world_agent.locations_info):
748
+ location_name = self.world_agent.find_location_name(location_code)
749
+ description = self.world_agent.locations_info[location_code]["description"]
750
+ location_info_text += f"\n{i+1}. {location_name}\nlocation_code:{location_code}\n{description}\n\n"
751
+ role_names = [f"{self.role_agents[code].role_name}({code})" for code in self.role_codes if self.role_agents[code].location_code == location_code]
752
+ role_names = ", ".join(role_names)
753
+ location_info_text += "目前在这里的角色有:" + role_names if self.language == "zh" else "Roles located here: " + role_names
754
+ else:
755
+ for i,location_code in enumerate(self.world_agent.locations_info):
756
+ location_name = self.world_agent.find_location_name(location_code)
757
+ role_names = [f"{self.role_agents[code].role_name}({code})" for code in self.role_codes if self.role_agents[code].location_code == location_code]
758
+ if len(role_names) == 0:continue
759
+ role_names = ", ".join(role_names)
760
+ location_info_text += f"【{location_name}】:" + role_names +";"
761
+ return location_info_text
762
+
763
+ def _name2code(self,roles):
764
+ name_dic = {self.role_agents[code].role_name:code for code in self.role_codes}
765
+ name_dic.update({self.role_agents[code].nickname:code for code in self.role_codes})
766
+ if isinstance(roles, list):
767
+ processed_roles = []
768
+ for role in roles:
769
+ if role in self.role_codes:
770
+ processed_roles.append(role)
771
+ elif role in name_dic:
772
+ processed_roles.append(name_dic[role])
773
+ elif "-" in role and role.split("-")[0] in name_dic:
774
+ processed_roles.append(name_dic[role.split("-")[0]])
775
+ elif role.replace("_","·") in self.role_codes:
776
+ processed_roles.append(role.replace("_","·"))
777
+ else:
778
+ processed_roles.append(role)
779
+ return processed_roles
780
+ elif isinstance(roles, str) :
781
+ roles = roles.replace("\n","")
782
+ if roles in self.role_codes:
783
+ return roles
784
+ elif roles in name_dic:
785
+ return name_dic[roles]
786
+ elif f"{roles}-{self.language}" in self.role_codes:
787
+ return f"{roles}-{self.language}"
788
+ elif "-" in roles and roles.split("-")[0] in name_dic:
789
+ return name_dic[roles.split("-")[0]]
790
+ elif roles.replace("_","·") in self.role_codes:
791
+ return roles.replace("_","·")
792
+ return roles
793
+
794
+ def log(self,text):
795
+ self.logger.info(text)
796
+ print(text)
797
+
798
+ def _save_current_simulation(self,
799
+ stage: Literal["location", "goal", "action"],
800
+ current_round: int = 0,
801
+ sub_round:int = 0):
802
+ """
803
+ Save the current simulation progress.
804
+
805
+ Args:
806
+ stage (Literal["location", "goal", "action"]): The stage in which the simulation has been carried out
807
+ current_round (int, optional): If the stage is "action", specify the number of rounds that have been completed. Defaults to 0.
808
+ """
809
+ if not self.if_save:
810
+ return
811
+ save_dir = f"./experiment_saves/{self.experiment_name}/{self.role_llm_name}/{self.start_time}"
812
+ create_dir(save_dir)
813
+ location_setted, goal_setted = False,False
814
+ if stage in ["location","goal","action"]:
815
+ location_setted = True
816
+ if stage in ["goal","action"]:
817
+ goal_setted = True
818
+ meta_info = {
819
+ "location_setted":location_setted,
820
+ "goal_setted": goal_setted,
821
+ "round": current_round,
822
+ "sub_round": sub_round,
823
+ }
824
+
825
+ save_json_file(os.path.join(save_dir, "meta_info.json"), meta_info)
826
+ name = self.experiment_name.split("/")[0]
827
+ save_json_file(os.path.join(save_dir, f"{name}.json"), self.config)
828
+
829
+ filename = os.path.join(save_dir, f"./server_info.json")
830
+ save_json_file(filename, self.__getstate__() )
831
+
832
+ self.history_manager.save_to_file(save_dir)
833
+ if hasattr(self, 'role_agents'):
834
+ for role_code in self.role_codes:
835
+ self.role_agents[role_code].save_to_file(save_dir)
836
+ self.world_agent.save_to_file(save_dir)
837
+
838
+ def continue_simulation_from_file(self, save_dir: str):
839
+ """
840
+ Restore the record of the last simulation.
841
+
842
+ Args:
843
+ save_dir (str): The path where the last simulation record was saved.
844
+
845
+ Returns:
846
+ Dict[str, Any]: The meta information recording the progress of the simulation
847
+ """
848
+ if os.path.exists(save_dir):
849
+ meta_info = load_json_file(os.path.join(save_dir, "./meta_info.json"))
850
+ filename = os.path.join(save_dir, f"./server_info.json")
851
+ states = load_json_file(filename)
852
+ self.__setstate__(states)
853
+ for role_code in self.role_codes:
854
+ self.role_agents[role_code].load_from_file(save_dir)
855
+ self.world_agent.load_from_file(save_dir)
856
+ self.history_manager.load_from_file(save_dir)
857
+
858
+ for record in self.history_manager.detailed_history:
859
+ for code in record["group"]:
860
+ if code in self.role_codes:
861
+ self.role_agents[code].record(record)
862
+ else:
863
+ meta_info = {
864
+ "location_setted":False,
865
+ "goal_setted": False,
866
+ "round": 0,
867
+ "sub_round": 0,
868
+ }
869
+ return meta_info
870
+
871
+ def __getstate__(self):
872
+ states = {key: value for key, value in self.__dict__.items() \
873
+ if isinstance(value, (str, int, list, dict, bool, type(None))) \
874
+ and key not in ['role_agents','world_agent','logger']}
875
+
876
+ return states
877
+
878
+ def __setstate__(self, states):
879
+ self.__dict__.update(states)
880
+
881
+
882
+ class BookWorld():
883
+ def __init__(self,
884
+ preset_path: str,
885
+ world_llm_name: str,
886
+ role_llm_name: str,
887
+ embedding_name:str = "bge-m3") :
888
+ self.server = Server(preset_path,
889
+ world_llm_name=world_llm_name,
890
+ role_llm_name=role_llm_name,
891
+ embedding_name=embedding_name)
892
+ self.selected_scene = None
893
+
894
+ def set_generator(self,
895
+ rounds:int = 10,
896
+ save_dir:str = "",
897
+ if_save: Literal[0,1] = 0,
898
+ mode: Literal["free", "script"] = "free",
899
+ scene_mode: Literal[0,1] = 0,):
900
+ self.server.continue_simulation_from_file(save_dir)
901
+ self.generator = self.server.simulate_generator(rounds = rounds,
902
+ save_dir = save_dir,
903
+ if_save = if_save,
904
+ mode = mode,
905
+ scene_mode = scene_mode)
906
+ def get_map_info(self):
907
+ location_codes = self.server.world_agent.locations
908
+ location_names = [self.server.world_agent.find_location_name(location_code) for location_code in location_codes]
909
+ n = len(location_codes)
910
+ distances = []
911
+ for i in range(n):
912
+ for j in range(i+1,n):
913
+ if self.server.world_agent.get_distance(location_codes[i], location_codes[j]):
914
+ distances.append({
915
+ "source": location_names[i],
916
+ "target": location_names[j],
917
+ "distance": self.server.world_agent.get_distance(location_codes[i], location_codes[j])
918
+ })
919
+
920
+ return {
921
+ "places": location_names,
922
+ "distances": distances
923
+ }
924
+ def select_scene(self,scene_number):
925
+ if scene_number == None:
926
+ self.selected_scene = scene_number
927
+ else:
928
+ self.selected_scene = str(scene_number)
929
+
930
+ def get_characters_info(self):
931
+ characters_info = []
932
+ if self.selected_scene == None:
933
+ codes = self.server.role_codes
934
+ else:
935
+ codes = self.server.scene_characters[str(self.selected_scene)]
936
+ for (i, code) in enumerate(codes):
937
+ agent = self.server.role_agents[code]
938
+ location = agent.location_name
939
+ if code in self.server.moving_roles_info:
940
+ location_name = self.server.world_agent.find_location_name(self.server.moving_roles_info[code]["location_code"])
941
+ distance = self.server.moving_roles_info[code]['distance']
942
+ location = f"Reaching {location_name}... ({distance})"
943
+ chara_info = {
944
+ "id": i,
945
+ "name": agent.nickname,
946
+ "icon": agent.icon_path,
947
+ "description": agent.role_profile,
948
+ "goal": agent.goal if agent.goal else agent.motivation,
949
+ "state": agent.status,
950
+ "location": location
951
+ }
952
+ characters_info.append(chara_info)
953
+ return characters_info
954
+
955
+ def generate_next_message(self):
956
+ message_type, code, text,message_id = next(self.generator)
957
+ if message_type == "role":
958
+ username = self.server.role_agents[code].role_name
959
+ icon_path = self.server.role_agents[code].icon_path
960
+ else:
961
+ username = message_type
962
+ icon_path = ""
963
+ message = {
964
+ 'username': username,
965
+ 'type': message_type, # role, world, system
966
+ 'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
967
+ 'text': text,
968
+ 'icon': icon_path,
969
+ "uuid": message_id,
970
+ "scene": self.server.cur_round
971
+ }
972
+ return message
973
+
974
+ def get_settings_info(self):
975
+ return self.server.world_agent.world_settings
976
+
977
+ def get_current_status(self):
978
+ status = self.server.current_status
979
+ status['event'] = self.server.event
980
+ group = []
981
+ for code in status['group']:
982
+ if code in self.server.role_codes:
983
+ group.append(self.server.role_agents[code].nickname)
984
+ else:
985
+ group.append(code)
986
+ status['group'] = group
987
+ location_code = self.server.current_status['location_code']
988
+ if location_code not in self.server.world_agent.locations_info:
989
+ location_name,location_description = "Undefined","Undefined"
990
+ else:
991
+ location_name,location_description = self.server.world_agent.find_location_name(location_code),self.server.world_agent.locations_info[location_code]["description"]
992
+ status['location'] = {'name': location_name, 'description': location_description}
993
+ return status
994
+
995
+ def handle_message_edit(self,record_id,new_text):
996
+ group = self.server.history_manager.modify_record(record_id,new_text)
997
+ for code in group:
998
+ self.server.role_agents[code].history_manager.modify_record(record_id,new_text)
999
+ return
1000
+
1001
+ def get_history_messages(self,save_dir):
1002
+
1003
+ messages = []
1004
+ for record in self.server.history_manager.detailed_history:
1005
+ message_type = record["actor_type"]
1006
+ code = record["role_code"]
1007
+ if message_type == "role":
1008
+ username = self.server.role_agents[code].role_name
1009
+ icon_path = self.server.role_agents[code].icon_path
1010
+ else:
1011
+ username = message_type
1012
+ icon_path = "./frontend/assets/images/default-icon.jpg"
1013
+ messages.append({
1014
+ 'username': username,
1015
+ 'type': message_type, # role, world, system
1016
+ 'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1017
+ 'text': record["detail"],
1018
+ 'icon': icon_path,
1019
+ "uuid": record["record_id"],
1020
+ "scene": record["cur_round"]
1021
+ })
1022
+ return messages
1023
+
1024
+ def generate_story(self,):
1025
+ logs = self.server.history_manager.get_complete_history()
1026
+ story = self.server.world_agent.log2story(logs)
1027
+ return story
1028
+
1029
+
1030
+
1031
+ if __name__ == "__main__":
1032
+ parser = argparse.ArgumentParser()
1033
+
1034
+ parser.add_argument('--world_llm', type=str, default='gpt-4o-mini')
1035
+ parser.add_argument('--role_llm', type=str, default='gpt-4o-mini')
1036
+ parser.add_argument('--genre', type=str, default='mgdv2')
1037
+ parser.add_argument('--preset_path', type=str, default='')
1038
+
1039
+ parser.add_argument('--if_save', type=int, default=1, choices=[0,1])
1040
+ parser.add_argument('--scene_mode', type=int, default=0, choices=[0,1])
1041
+ parser.add_argument('--rounds', type=int, default=10)
1042
+ parser.add_argument('--save_dir', type=str, default='')
1043
+ parser.add_argument('--mode', type=str, default='free', choices=['free','script'])
1044
+ args = parser.parse_args()
1045
+ world_llm_name = args.world_llm
1046
+ role_llm_name = args.role_llm
1047
+ rounds = args.rounds
1048
+ genre = args.genre
1049
+ preset_path = args.preset_path
1050
+ save_dir = args.save_dir
1051
+ if_save = args.if_save
1052
+ scene_mode = args.scene_mode
1053
+ mode = args.mode
1054
+ if not preset_path:
1055
+ preset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),f"./config/experiment_{genre}.json")
1056
+
1057
+ bw = BookWorld(preset_path, world_llm_name=world_llm_name, role_llm_name=role_llm_name)
1058
+ bw.set_generator(rounds = rounds, save_dir = save_dir, if_save = if_save, scene_mode = scene_mode,mode = mode)
1059
+
1060
+ for i in range(100):
1061
+ try:
1062
+ bw.generate_next_message()
1063
+ except StopIteration:
1064
+ break
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+
6
+ ENV HOME=/home/user \
7
+ PATH=/home/user/.local/bin:$PATH
8
+ WORKDIR $HOME/app
9
+
10
+ COPY --chown=user . $HOME/app
11
+
12
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
13
+
14
+ RUN huggingface-cli download BAAI/bge-m3
15
+
16
+ ENV OPENAI_API_KEY=""
17
+
18
+ EXPOSE 8000
19
+
20
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
app.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import HTMLResponse, FileResponse
4
+ import uvicorn
5
+ import json
6
+ import asyncio
7
+ import os
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+ from bw_utils import is_image, load_json_file
11
+ from BookWorld import BookWorld
12
+
13
+ app = FastAPI()
14
+ default_icon_path = './frontend/assets/images/default-icon.jpg'
15
+ config = load_json_file('config.json')
16
+ for key in config:
17
+ if "API_KEY" in key:
18
+ os.environ[key] = config[key]
19
+
20
+ static_file_abspath = os.path.abspath(os.path.join(os.path.dirname(__file__), 'frontend'))
21
+ app.mount("/frontend", StaticFiles(directory=static_file_abspath), name="frontend")
22
+
23
+ class ConnectionManager:
24
+ def __init__(self):
25
+ self.active_connections: dict[str, WebSocket] = {}
26
+ self.story_tasks: dict[str, asyncio.Task] = {}
27
+ if True:
28
+ if "preset_path" in config and config["preset_path"] and os.path.exists(config["preset_path"]):
29
+ preset_path = config["preset_path"]
30
+ elif "genre" in config and config["genre"]:
31
+ genre = config["genre"]
32
+ preset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),f"./config/experiment_{genre}.json")
33
+ else:
34
+ raise ValueError("Please set the preset_path in `config.json`.")
35
+ self.bw = BookWorld(preset_path = preset_path,
36
+ world_llm_name = config["world_llm_name"],
37
+ role_llm_name = config["role_llm_name"],
38
+ embedding_name = config["embedding_model_name"])
39
+ self.bw.set_generator(rounds = config["rounds"],
40
+ save_dir = config["save_dir"],
41
+ if_save = config["if_save"],
42
+ mode = config["mode"],
43
+ scene_mode = config["scene_mode"],)
44
+ else:
45
+ from BookWorld_test import BookWorld_test
46
+ self.bw = BookWorld_test()
47
+
48
+ async def connect(self, websocket: WebSocket, client_id: str):
49
+ await websocket.accept()
50
+ self.active_connections[client_id] = websocket
51
+
52
+ def disconnect(self, client_id: str):
53
+ if client_id in self.active_connections:
54
+ del self.active_connections[client_id]
55
+ self.stop_story(client_id)
56
+
57
+ def stop_story(self, client_id: str):
58
+ if client_id in self.story_tasks:
59
+ self.story_tasks[client_id].cancel()
60
+ del self.story_tasks[client_id]
61
+
62
+ async def start_story(self, client_id: str):
63
+ if client_id in self.story_tasks:
64
+ # 如果已经有任务在运行,先停止它
65
+ self.stop_story(client_id)
66
+
67
+ # 创建新的故事任务
68
+ self.story_tasks[client_id] = asyncio.create_task(
69
+ self.generate_story(client_id)
70
+ )
71
+
72
+ async def generate_story(self, client_id: str):
73
+ """持续生成故事的协程"""
74
+ try:
75
+ while True:
76
+ if client_id in self.active_connections:
77
+ message,status = await self.get_next_message()
78
+ await self.active_connections[client_id].send_json({
79
+ 'type': 'message',
80
+ 'data': message
81
+ })
82
+ await self.active_connections[client_id].send_json({
83
+ 'type': 'status_update',
84
+ 'data': status
85
+ })
86
+ # 添加延迟,控制消息发送频率
87
+ await asyncio.sleep(1) # 可以调整这个值
88
+ else:
89
+ break
90
+ except asyncio.CancelledError:
91
+ # 任务被取消时的处理
92
+ print(f"Story generation cancelled for client {client_id}")
93
+ except Exception as e:
94
+ print(f"Error in generate_story: {e}")
95
+
96
+ async def get_initial_data(self):
97
+ """获取初始化数据"""
98
+ return {
99
+ 'characters': self.bw.get_characters_info(),
100
+ 'map': self.bw.get_map_info(),
101
+ 'settings': self.bw.get_settings_info(),
102
+ 'status': self.bw.get_current_status(),
103
+ 'history_messages':self.bw.get_history_messages(save_dir = config["save_dir"]),
104
+ }
105
+
106
+ async def get_next_message(self):
107
+ """从BookWorld获取下一条消息"""
108
+ message = self.bw.generate_next_message()
109
+ if not is_image(message["icon"]):
110
+ message["icon"] = default_icon_path
111
+ status = self.bw.get_current_status()
112
+ return message,status
113
+
114
+ manager = ConnectionManager()
115
+
116
+ @app.get("/")
117
+ async def get():
118
+ html_file = Path("index.html")
119
+ return HTMLResponse(html_file.read_text())
120
+
121
+ @app.get("/data/{full_path:path}")
122
+ async def get_file(full_path: str):
123
+ # 可以设置多个基础路径
124
+ base_paths = [
125
+ Path("/data/")
126
+ ]
127
+
128
+ for base_path in base_paths:
129
+ file_path = base_path / full_path
130
+ if file_path.exists() and file_path.is_file():
131
+ return FileResponse(file_path)
132
+ else:
133
+ return FileResponse(default_icon_path)
134
+
135
+ raise HTTPException(status_code=404, detail="File not found")
136
+
137
+ @app.websocket("/ws/{client_id}")
138
+ async def websocket_endpoint(websocket: WebSocket, client_id: str):
139
+ await manager.connect(websocket, client_id)
140
+ try:
141
+ initial_data = await manager.get_initial_data()
142
+ await websocket.send_json({
143
+ 'type': 'initial_data',
144
+ 'data': initial_data
145
+ })
146
+
147
+ while True:
148
+ data = await websocket.receive_text()
149
+ message = json.loads(data)
150
+
151
+ if message['type'] == 'user_message':
152
+ # 处理用户消息
153
+ await websocket.send_json({
154
+ 'type': 'message',
155
+ 'data': {
156
+ 'username': 'User',
157
+ 'timestamp': message['timestamp'],
158
+ 'text': message['text'],
159
+ 'icon': default_icon_path,
160
+ }
161
+ })
162
+
163
+ elif message['type'] == 'control':
164
+ # 处理控制命令
165
+ if message['action'] == 'start':
166
+ await manager.start_story(client_id)
167
+ elif message['action'] == 'pause':
168
+ manager.stop_story(client_id)
169
+ elif message['action'] == 'stop':
170
+ manager.stop_story(client_id)
171
+ # 可以在这里添加额外的停止逻辑
172
+
173
+ elif message['type'] == 'edit_message':
174
+ # 处理消息编辑
175
+ edit_data = message['data']
176
+ # 假设 BookWorld 类有一个处理编辑的方法
177
+ manager.bw.handle_message_edit(
178
+ record_id=edit_data['uuid'],
179
+ new_text=edit_data['text']
180
+ )
181
+
182
+ elif message['type'] == 'request_scene_characters':
183
+ manager.bw.select_scene(message['scene'])
184
+ scene_characters = manager.bw.get_characters_info()
185
+ await websocket.send_json({
186
+ 'type': 'scene_characters',
187
+ 'data': scene_characters
188
+ })
189
+
190
+ elif message['type'] == 'generate_story':
191
+ # 生成故事文本
192
+ story_text = manager.bw.generate_story()
193
+ # 发送生成的故事作为新消息
194
+ await websocket.send_json({
195
+ 'type': 'message',
196
+ 'data': {
197
+ 'username': 'System',
198
+ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
199
+ 'text': story_text,
200
+ 'icon': default_icon_path,
201
+ 'type': 'story'
202
+ }
203
+ })
204
+
205
+ elif message['type'] == 'request_api_configs':
206
+ await websocket.send_json({
207
+ 'type': 'api_configs',
208
+ 'data': API_CONFIGS
209
+ })
210
+
211
+ elif message['type'] == 'api_settings':
212
+ # 处理API设置
213
+ settings = message['data']
214
+ # 设置环境变量
215
+ os.environ[settings['envKey']] = settings['apiKey']
216
+
217
+ # 更新BookWorld的设置
218
+ manager.bw.update_api_settings(
219
+ provider=settings['provider'],
220
+ model=settings['model']
221
+ )
222
+
223
+ # 发送确认消息
224
+ await websocket.send_json({
225
+ 'type': 'message',
226
+ 'data': {
227
+ 'username': 'System',
228
+ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
229
+ 'text': f'已更新 {settings["provider"]} API设置',
230
+ 'icon': default_icon_path,
231
+ 'type': 'system'
232
+ }
233
+ })
234
+ except Exception as e:
235
+ print(f"WebSocket error: {e}")
236
+ finally:
237
+ manager.disconnect(client_id)
238
+
239
+ if __name__ == "__main__":
240
+ uvicorn.run("server:app", host="0.0.0.0", port=8000, reload=True)
bw_utils.py ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import json
4
+ import torch
5
+ import logging
6
+ import datetime
7
+ import re
8
+ import random
9
+ import base64
10
+
11
+ MODEL_NAME_DICT = {
12
+ "gpt3":"openai/gpt-3.5-turbo",
13
+ "gpt-4":"openai/gpt-4",
14
+ "gpt-4o":"openai/gpt-4o",
15
+ "gpt-4o-mini":"openai/gpt-4o-mini",
16
+ "gpt-3.5-turbo":"openai/gpt-3.5-turbo",
17
+ "deepseek-r1":"deepseek/deepseek-r1",
18
+ "deepseek-v3":"deepseek/deepseek-chat",
19
+ "gemini-2":"google/gemini-2.0-flash-001",
20
+ "gemini-1.5":"google/gemini-flash-1.5",
21
+ "llama3-70b": "meta-llama/llama-3.3-70b-instruct",
22
+ "qwen-turbo":"qwen/qwen-turbo",
23
+ "qwen-plus":"qwen/qwen-plus",
24
+ "qwen-max":"qwen/qwen-max",
25
+ "qwen-2.5-72b":"qwen/qwen-2.5-72b-instruct",
26
+ "claude-3.5-sonnet":"anthropic/claude-3.5-sonnet",
27
+ "phi-4":"microsoft/phi-4",
28
+ }
29
+
30
+ def get_models(model_name):
31
+ if os.getenv("OPENROUTER_API_KEY", default="") and model_name in MODEL_NAME_DICT:
32
+ from modules.llm.OpenRouter import OpenRouter
33
+ return OpenRouter(model=MODEL_NAME_DICT[model_name])
34
+ elif model_name.startswith('gpt-3.5'):
35
+ from modules.llm.LangChainGPT import LangChainGPT
36
+ return LangChainGPT(model="gpt-3.5-turbo")
37
+ elif model_name == 'gpt-4':
38
+ from modules.llm.LangChainGPT import LangChainGPT
39
+ return LangChainGPT(model="gpt-4")
40
+ elif model_name == 'gpt-4-turbo':
41
+ from modules.llm.LangChainGPT import LangChainGPT
42
+ return LangChainGPT(model="gpt-4")
43
+ elif model_name == 'gpt-4o':
44
+ from modules.llm.LangChainGPT import LangChainGPT
45
+ return LangChainGPT(model="gpt-4o")
46
+ elif model_name == "gpt-4o-mini":
47
+ from modules.llm.LangChainGPT import LangChainGPT
48
+ return LangChainGPT(model="gpt-4o-mini")
49
+ elif model_name.startswith("claude"):
50
+ from modules.llm.LangChainGPT import LangChainGPT
51
+ return LangChainGPT(model="claude-3-5-sonnet-20241022")
52
+ elif model_name.startswith('qwen'):
53
+ from modules.llm.Qwen import Qwen
54
+ return Qwen(model = model_name)
55
+ elif model_name.startswith('deepseek'):
56
+ from modules.llm.DeepSeek import DeepSeek
57
+ return DeepSeek()
58
+ elif model_name.startswith('doubao'):
59
+ from modules.llm.Doubao import Doubao
60
+ return Doubao()
61
+ elif model_name.startswith('gemini'):
62
+ from modules.llm.Gemini import Gemini
63
+ return Gemini()
64
+ else:
65
+ print(f'Warning! undefined model {model_name}, use gpt-3.5-turbo instead.')
66
+ from modules.llm.LangChainGPT import LangChainGPT
67
+ return LangChainGPT()
68
+
69
+ def build_world_agent_data(world_file_path,max_words = 30):
70
+ world_dir = os.path.dirname(world_file_path)
71
+ details_dir = os.path.join(world_dir,"./world_details")
72
+ data = []
73
+ settings = []
74
+ if os.path.exists(details_dir):
75
+ for path in get_child_paths(details_dir):
76
+ if os.path.splitext(path)[-1] == ".txt":
77
+ text = load_text_file(path)
78
+ data += split_text_by_max_words(text,max_words)
79
+ if os.path.splitext(path)[-1] == ".jsonl":
80
+ jsonl = load_jsonl_file(path)
81
+ data += [f"{dic['term']}:{dic['detail']}" for dic in jsonl]
82
+ settings += jsonl
83
+ return data,settings
84
+
85
+ def build_db(data, db_name, db_type, embedding, save_type="persistent"):
86
+ if True:
87
+ from modules.db.ChromaDB import ChromaDB
88
+ db = ChromaDB(embedding,save_type)
89
+ db_name = db_name
90
+ db.init_from_data(data,db_name)
91
+ return db
92
+
93
+ def get_root_dir():
94
+ current_file_path = os.path.abspath(__file__)
95
+ root_dir = os.path.dirname(current_file_path)
96
+ return root_dir
97
+
98
+ def create_dir(dirname):
99
+ if not os.path.exists(dirname):
100
+ os.makedirs(dirname)
101
+
102
+ def get_logger(experiment_name):
103
+ logger = logging.getLogger(experiment_name)
104
+ logger.setLevel(logging.INFO)
105
+ current_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
106
+ create_dir(f"{get_root_dir()}/log/{experiment_name}")
107
+ file_handler = logging.FileHandler(os.path.join(get_root_dir(),f"./log/{experiment_name}/{current_time}.log"),encoding='utf-8')
108
+ file_handler.setLevel(logging.INFO)
109
+
110
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
111
+ file_handler.setFormatter(formatter)
112
+
113
+ logger.addHandler(file_handler)
114
+
115
+ # Avoid logging duplication
116
+ logger.propagate = False
117
+
118
+ return logger
119
+
120
+ def merge_text_with_limit(text_list, max_words, language = 'en'):
121
+ """
122
+ Merge a list of text strings into one, stopping when adding another text exceeds the maximum count.
123
+
124
+ Args:
125
+ text_list (list): List of strings to be merged.
126
+ max_count (int): Maximum number of characters (for Chinese) or words (for English).
127
+ is_chinese (bool): If True, count Chinese characters; if False, count English words.
128
+
129
+ Returns:
130
+ str: The merged text, truncated as needed.
131
+ """
132
+ merged_text = ""
133
+ current_count = 0
134
+
135
+ for text in text_list:
136
+ if language == 'zh':
137
+ # Count Chinese characters
138
+ text_length = len(text)
139
+ else:
140
+ # Count English words
141
+ text_length = len(text.split(" "))
142
+
143
+ if current_count + text_length > max_words:
144
+ break
145
+
146
+ merged_text += text + "\n"
147
+ current_count += text_length
148
+
149
+ return merged_text
150
+
151
+ def normalize_string(text):
152
+ # 去除空格并将所有字母转为小写
153
+ import re
154
+ return re.sub(r'[\s\,\;\t\n]+', '', text).lower()
155
+
156
+ def fuzzy_match(str1, str2, threshold=0.8):
157
+ str1_normalized = normalize_string(str1)
158
+ str2_normalized = normalize_string(str2)
159
+
160
+ if str1_normalized == str2_normalized:
161
+ return True
162
+
163
+ return False
164
+
165
+ def load_character_card(path):
166
+ from PIL import Image
167
+ import PIL.PngImagePlugin
168
+
169
+ image = Image.open(path)
170
+ if isinstance(image, PIL.PngImagePlugin.PngImageFile):
171
+ for key, value in image.text.items():
172
+ try:
173
+ character_info = json.loads(decode_base64(value))
174
+ if character_info:
175
+ return character_info
176
+ except:
177
+ continue
178
+ return None
179
+
180
+ def decode_base64(encoded_string):
181
+ # Convert the string to bytes if it's not already
182
+ if isinstance(encoded_string, str):
183
+ encoded_bytes = encoded_string.encode('ascii')
184
+ else:
185
+ encoded_bytes = encoded_string
186
+
187
+ # Decode the Base64 bytes
188
+ decoded_bytes = base64.b64decode(encoded_bytes)
189
+
190
+ # Try to convert the result to a string, assuming UTF-8 encoding
191
+ try:
192
+ decoded_string = decoded_bytes.decode('utf-8')
193
+ return decoded_string
194
+ except UnicodeDecodeError:
195
+ # If it's not valid UTF-8 text, return the raw bytes
196
+ return decoded_bytes
197
+
198
+ def remove_list_elements(list1, *args):
199
+ for target in args:
200
+ if isinstance(target,list) or isinstance(target,dict):
201
+ list1 = [i for i in list1 if i not in target]
202
+ else:
203
+ list1 = [i for i in list1 if i != target]
204
+ return list1
205
+
206
+ def extract_html_content(html):
207
+ from bs4 import BeautifulSoup
208
+ soup = BeautifulSoup(html, "html.parser")
209
+
210
+ content_div = soup.find("div", {"id": "content"})
211
+ if not content_div:
212
+ return ""
213
+
214
+ paragraphs = []
215
+ for div in content_div.find_all("div"):
216
+ paragraphs.append(div.get_text(strip=True))
217
+
218
+ main_content = "\n\n".join(paragraphs)
219
+ return main_content
220
+
221
+ def load_text_file(path):
222
+ with open(path,"r",encoding="utf-8") as f:
223
+ text = f.read()
224
+ return text
225
+
226
+ def save_text_file(path,target):
227
+ with open(path,"w",encoding="utf-8") as f:
228
+ text = f.write(target)
229
+
230
+ def load_json_file(path):
231
+ with open(path,"r",encoding="utf-8") as f:
232
+ return json.load(f)
233
+
234
+ def save_json_file(path,target):
235
+ dir_name = os.path.dirname(path)
236
+ if not os.path.exists(dir_name):
237
+ os.makedirs(dir_name)
238
+ with open(path,"w",encoding="utf-8") as f:
239
+ json.dump(target, f, ensure_ascii=False,indent=True)
240
+
241
+ def load_jsonl_file(path):
242
+ data = []
243
+ with open(path,"r",encoding="utf-8") as f:
244
+ for line in f:
245
+ data.append(json.loads(line))
246
+ return data
247
+
248
+ def save_jsonl_file(path,target):
249
+ with open(path, "w",encoding="utf-8") as f:
250
+ for row in target:
251
+ print(json.dumps(row, ensure_ascii=False), file=f)
252
+
253
+ def split_text_by_max_words(text: str, max_words: int = 30):
254
+ segments = []
255
+ current_segment = []
256
+ current_length = 0
257
+
258
+ lines = text.splitlines()
259
+
260
+ for line in lines:
261
+ words_in_line = len(line)
262
+ current_segment.append(line + '\n')
263
+ current_length += words_in_line
264
+
265
+ if current_length + words_in_line > max_words:
266
+ segments.append(''.join(current_segment))
267
+ current_segment = []
268
+ current_length = 0
269
+
270
+ if current_segment:
271
+ segments.append(''.join(current_segment))
272
+
273
+ return segments
274
+
275
+ def lang_detect(text):
276
+ import re
277
+ def count_chinese_characters(text):
278
+ # 使用正则表达式匹配所有汉字字符
279
+ chinese_chars = re.findall(r'[\u4e00-\u9fff]', text)
280
+ return len(chinese_chars)
281
+
282
+ if count_chinese_characters(text) > len(text) * 0.05:
283
+ lang = 'zh'
284
+ else:
285
+ lang = 'en'
286
+ return lang
287
+
288
+ def dict_to_str(dic):
289
+ res = ""
290
+ for key in dic:
291
+ res += f"{key}: {dic[key]};"
292
+ return res
293
+
294
+ def count_tokens_num(string, encoding_name = "cl100k_base"):
295
+ encoding = tiktoken.get_encoding(encoding_name)
296
+ num_tokens = len(encoding.encode(string))
297
+ return num_tokens
298
+
299
+
300
+ def json_parser(output):
301
+ output = output.replace("\n", "")
302
+ output = output.replace("\t", "")
303
+ if "{" not in output:
304
+ output = "{" + output
305
+ if "}" not in output:
306
+ output += "}"
307
+ pattern = r'\{.*\}'
308
+ matches = re.findall(pattern, output, re.DOTALL)
309
+ try:
310
+ parsed_json = eval(matches[0])
311
+ except:
312
+ try:
313
+ parsed_json = json.loads(matches[0])
314
+
315
+ except json.JSONDecodeError:
316
+ try:
317
+ detail = re.search(r'"detail":\s*(.+?)\s*}', matches[0]).group(1)
318
+ detail = f"\"{detail}\""
319
+ new_output = re.sub(r'"detail":\s*(.+?)\s*}', f"\"detail\":{detail}}}", matches[0])
320
+ parsed_json = json.loads(new_output)
321
+ except Exception as e:
322
+ raise ValueError("No valid JSON found in the input string")
323
+ return parsed_json
324
+
325
+ def action_detail_decomposer(detail):
326
+ thoughts = re.findall(r'【(.*?)】', detail)
327
+ actions = re.findall(r'((.*?))', detail)
328
+ dialogues = re.findall(r'「(.*?)」', detail)
329
+ return thoughts,actions,dialogues
330
+
331
+ def conceal_thoughts(detail):
332
+ text = re.sub(r'【.*?】', '', detail)
333
+ text = re.sub(r'\[.*?\]', '', text)
334
+ return text
335
+
336
+ def extract_first_number(text):
337
+ match = re.search(r'\b\d+(?:\.\d+)?\b', text)
338
+ return int(match.group()) if match else None
339
+
340
+ def check_role_code_availability(role_code,role_file_dir):
341
+ for path in get_grandchild_folders(role_file_dir):
342
+ if role_code in path:
343
+ return True
344
+ return False
345
+
346
+ def get_grandchild_folders(root_folder):
347
+ folders = []
348
+ for resource in os.listdir(root_folder):
349
+ subpath = os.path.join(root_folder,resource)
350
+ for folder_name in os.listdir(subpath):
351
+ folder_path = os.path.join(subpath, folder_name)
352
+ folders.append(folder_path)
353
+
354
+ return folders
355
+
356
+ def get_child_folders(root_folder):
357
+ folders = []
358
+ for resource in os.listdir(root_folder):
359
+ path = os.path.join(root_folder,resource)
360
+ if os.path.isdir(path):
361
+ folders.append(path)
362
+ return folders
363
+
364
+ def get_child_paths(root_folder):
365
+ paths = []
366
+ for resource in os.listdir(root_folder):
367
+ path = os.path.join(root_folder,resource)
368
+ if os.path.isfile(path):
369
+ paths.append(path)
370
+ return paths
371
+
372
+ def get_first_directory(path):
373
+ try:
374
+ for item in os.listdir(path):
375
+ full_path = os.path.join(path, item)
376
+ if os.path.isdir(full_path):
377
+ return full_path
378
+ return None
379
+ except Exception as e:
380
+ print(f"Error: {e}")
381
+ return None
382
+
383
+ def find_files_with_suffix(directory, suffix):
384
+ matched_files = []
385
+ for root, dirs, files in os.walk(directory): # 遍历目录及其子目录
386
+ for file in files:
387
+ if file.endswith(suffix): # 检查文件后缀
388
+ matched_files.append(os.path.join(root, file)) # 将符合条件的文件路径加入列表
389
+
390
+ return matched_files
391
+
392
+ def remove_element_with_probability(lst, threshold=3, probability=0.2):
393
+ # 确保列表不为空
394
+ if len(lst) > threshold and random.random() < probability:
395
+ # 随机选择一个元素的索引
396
+ index = random.randint(0, len(lst) - 1)
397
+ # 删除该索引位置的元素
398
+ lst.pop(index)
399
+ return lst
400
+
401
+ def count_token_num(text):
402
+ from transformers import GPT2TokenizerFast
403
+ tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
404
+ return len(tokenizer.encode(text))
405
+
406
+ def get_cost(model_name,prompt,output):
407
+ input_price=0
408
+ output_price=0
409
+ if model_name.startswith("gpt-4"):
410
+ input_price=10
411
+ output_price=30
412
+ elif model_name.startswith("gpt-3.5"):
413
+ input_price=0.5
414
+ output_price=1.5
415
+
416
+ return input_price*count_token_num(prompt)/1000000 + output_price * count_token_num(output)/1000000
417
+
418
+ def is_image(filepath):
419
+ if not os.path.isfile(filepath):
420
+ return False
421
+
422
+ valid_image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff','.webp']
423
+ file_extension = os.path.splitext(filepath)[1].lower()
424
+
425
+ # 判断扩展名是否在有效图片扩展名列表中
426
+ if file_extension in valid_image_extensions:
427
+ return True
428
+
429
+ return False
430
+
431
+ def clean_collection_name(name: str) -> str:
432
+ cleaned_name = name.replace(' ', '_')
433
+ cleaned_name = cleaned_name.replace('.', '_')
434
+ if not all(ord(c) < 128 for c in cleaned_name):
435
+ encoded = base64.b64encode(cleaned_name.encode('utf-8')).decode('ascii')
436
+ encoded = encoded[:60] if len(encoded) > 60 else encoded
437
+ valid_name = f"mem_{encoded}"
438
+ else:
439
+ valid_name = cleaned_name
440
+ valid_name = re.sub(r'[^a-zA-Z0-9_-]', '-', valid_name)
441
+ valid_name = re.sub(r'\.\.+', '-', valid_name)
442
+ valid_name = re.sub(r'^[^a-zA-Z0-9]+', '', valid_name) # 移除开头非法字符
443
+ valid_name = re.sub(r'[^a-zA-Z0-9]+$', '', valid_name)
444
+ return valid_name
445
+
446
+ cache_sign = True
447
+ cache = None
448
+ def cached(func):
449
+ def wrapper(*args,**kwargs):
450
+ global cache
451
+ cache_path = "bw_cache.pkl"
452
+ if cache == None:
453
+ if not os.path.exists(cache_path):
454
+ cache = {}
455
+ else:
456
+ cache = pickle.load(open(cache_path, 'rb'))
457
+ key = (func.__name__, str([args[0].role_code, args[0].__class__, args[0].llm_name , args[0].history]), str(kwargs.items()))
458
+ if (cache_sign and key in cache and cache[key] not in [None, '[TOKEN LIMIT]']) :
459
+ return cache[key]
460
+ else:
461
+ result = func(*args, **kwargs)
462
+ if result != 'busy' and result != None:
463
+ cache[key] = result
464
+ pickle.dump(cache, open(cache_path, 'wb'))
465
+ return result
466
+ return wrapper
config.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_llm_name": "gemini-2",
3
+ "world_llm_name": "gemini-2",
4
+ "embedding_model_name":"bge-m3",
5
+ "preset_path":"./experiment_presets/experiment_alice.json",
6
+ "if_save": 0,
7
+ "scene_mode": 1,
8
+ "rounds": 10,
9
+ "save_dir": "",
10
+ "mode": "script",
11
+
12
+ "OPENAI_API_KEY":"",
13
+ "GEMINI_API_KEY":"",
14
+ "ANTHROPIC_API_KEY":"",
15
+ "OPENROUTER_API_KEY":"",
16
+ "DASHSCOPE_API_KEY":"",
17
+ "DEEPSEEK_API_KEY":"",
18
+ "ARK_API_KEY":""
19
+
20
+ }
convert_sillytavern_cards_to_data.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ from bw_utils import get_child_paths,lang_detect,is_image,decode_base64,save_json_file,create_dir
3
+ import PIL.PngImagePlugin
4
+ import os
5
+ import json
6
+
7
+ card_dir = "./data/sillytavern_cards"
8
+ names = os.listdir(card_dir)
9
+
10
+ for name in names:
11
+ path = os.path.join(card_dir, name)
12
+ role_code = name.split('.')[0].replace(" ","_")
13
+ if is_image(path):
14
+ with open(path, 'rb') as f:
15
+ image = Image.open(f)
16
+ card_info = json.loads(decode_base64(image.text['chara']))
17
+ language = lang_detect(card_info['data']['description'])
18
+ role_info = {
19
+ "role_code": f"{role_code}-{language}",
20
+ "role_name": card_info['data']['name'],
21
+ "source": "",
22
+ "profile": card_info['data']['description'],
23
+ "nickname": card_info['data']['name'],
24
+ "relation": {},
25
+ "card_data": card_info['data']
26
+ }
27
+ create_dir(f"./data/roles/sillytavern/{role_code}")
28
+ save_json_file(os.path.join(f"./data/roles/sillytavern/{role_code}","role_info.json"),role_info)
29
+ image.save(os.path.join(f"./data/roles/sillytavern/{role_code}",f"icon.png"))
30
+ print(f"{name} converted successfully.")
31
+
data/locations/A_Song_of_Ice_and_Fire.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Winterfell": {
3
+ "location_name": "临冬城",
4
+ "location_code": "Winterfell",
5
+ "description": "临冬城是北境的主要城堡,也是史塔克家族的家园。",
6
+ "detail": "临冬城坐落在寒冷的北方,由灰色石头建造,坚固而古老。城堡由内城和外城组成,内城有高大的城墙和几座塔楼,外城有许多庭院、马厩和士兵宿舍。城堡中心有一个温泉池,水源来自地下的温泉,给寒冷的临冬城带来一丝温暖。临冬城的神木林是北境人心目中神圣的地方,那里有一棵古老的心树,是史塔克家族的祈祷之地。",
7
+ "special_actions": ["会议", "祈祷", "训练"]
8
+ },
9
+ "King's Landing": {
10
+ "location_name": "君临城",
11
+ "location_code": "King's Landing",
12
+ "description": "君临城是维斯特洛的首都,铁王座的所在地。",
13
+ "detail": "君临城坐落在黑水河的岸边,是维斯特洛最大的城市。城中最显著的建筑是红堡,铁王座就位于其中的铁王座厅。君临城的街道狭窄而繁忙,挤满了商人、士兵和各种各样的人。市集、酒馆和贫民区都充满了喧嚣和活力。红堡外还有祈祷圣堂、高塔以及黑水河边的港口,所有这些构成了君临城独特的风貌。",
14
+ "special_actions": ["统治", "阴谋", "战争"]
15
+ },
16
+ "The Wall": {
17
+ "location_name": "长城",
18
+ "location_code": "The Wall",
19
+ "description": "长城是守护维斯特洛北方边境的巨大冰墙。",
20
+ "detail": "长城由坚固的冰和雪建成,绵延数百里,横亘在维斯特洛北境与绝境长城之外的野地之间。它高耸入云,常年被冰雪覆盖。长城由守夜人守护,他们在这里驻扎,抵御来自北方的威胁。长城上有许多堡垒和哨塔,最大的堡垒是黑城堡,它是守夜人的总部。长城的尽头是东海望,一个位于海边的小镇,是守夜人和北境世界之间的主要通道。",
21
+ "special_actions": ["守卫", "巡逻", "抵御异鬼"]
22
+ },
23
+ "Dragonstone": {
24
+ "location_name": "龙石岛",
25
+ "location_code": "Dragonstone",
26
+ "description": "龙石岛是坦格利安家族的古老领地,位于黑水湾的一个岛屿上。",
27
+ "detail": "龙石岛是一座由火山岩形成的小岛,岛上有一座古老而神秘的城堡。城堡的塔楼和城墙由黑色的火山岩建成,显得阴森而强大。龙石岛曾是坦格利安家族的基地,他们在这里起家,征服了七大王国。龙石岛的地理位置使它成为一个战略要地,控制着通往君临城的海路。岛上的龙穴曾是坦格利安家族饲养龙的地方,虽然龙已经消失,但岛上的遗迹仍然充满着他们的传说。",
28
+ "special_actions": ["战略会议", "巩固统治", "策划复仇"]
29
+ },
30
+ "The Eyrie": {
31
+ "location_name": "鹰巢城",
32
+ "location_code": "The Eyrie",
33
+ "description": "鹰巢城是维斯特洛最难以攻陷的城堡,位于高耸的山巅。",
34
+ "detail": "鹰巢城坐落在巍峨的山顶之上,高高在云端,仿佛悬在半空中。城堡由巨石建造,只有通过一条陡峭的山道才能到达,极难进攻。城堡内的高墙和塔楼都象征着它的坚不可摧。鹰巢城是艾林家族的居所,他们统治着山谷地区。城堡内部富丽堂皇,但也寒冷而孤立。鹰巢城的月门是一道令人恐惧的悬空门,从这里可以将罪犯直接抛下悬崖,坠入深渊。",
35
+ "special_actions": ["裁决", "坚守", "眺望"]
36
+ },
37
+ "TwinTowers": {
38
+ "location_name": "孪河城",
39
+ "location_code": "TwinTowers",
40
+ "description": "孪河城是弗雷家族的城堡,位于河间地的重要战略位置。",
41
+ "detail": "孪河城坐落在红叉河北岸,是河间地弗雷家族的领地。城堡由两座高塔组成,耸立于红叉河两侧,由一座坚固的桥梁连接,控制着通向北境和南部的重要通道。弗雷家族通过这座桥梁收取过桥费,积累了大量财富。孪河城不仅在地理位置上显得至关重要,还以其家族的狡诈和复杂的内部关系而闻名。城堡内装饰简朴,冷酷而功能性强。它是血色婚礼发生的场所,弗雷家族在这里背叛了史塔克家族。",
42
+ "special_actions": ["背叛", "封锁", "谈判"]
43
+ }
44
+
45
+ }
data/locations/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "somewhere": {
3
+ "location_code": "somewhere",
4
+ "location_name": "somewhere",
5
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
6
+ "description": "Some where in the world of Alice’s Adventures in Wonderland",
7
+ "detail": ""
8
+ }
9
+ }
data/locations/example_locations.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "laboratory": {
3
+ "location_code": "laboratory",
4
+ "location_name": "Laboratory",
5
+ "source": "example",
6
+ "description": "A high-security research lab pioneering human-machine fusion experiments.",
7
+ "detail": "Inside this sterile, high-tech facility, scientists and engineers work on merging biological and artificial systems. It stands at the forefront of redefining human evolution through machine integration."
8
+ },
9
+ "university": {
10
+ "location_code": "university",
11
+ "location_name": "University",
12
+ "source": "example",
13
+ "description": "An elite institution training the next generation of technologists and bioengineers.",
14
+ "detail": "The university offers cutting-edge programs in AI ethics, cybernetics, and synthetic biology. Its sprawling campus nurtures innovation, producing thinkers who will shape humanity’s future relationship with machines."
15
+ },
16
+ "eldridge_corporation": {
17
+ "location_code": "eldridge_corporation",
18
+ "location_name": "Eldridge Corporation",
19
+ "source": "example",
20
+ "description": "A dominant tech conglomerate driving the frontier of AI and biotechnology.",
21
+ "detail": "Eldridge Corporation controls vast sectors of AI research, cybernetic enhancements, and neural networking. Officially a beacon of progress, rumors persist of secret projects far beyond ethical boundaries."
22
+ }
23
+ }
24
+
data/maps/A_Song_of_Ice_and_Fire.csv ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ,Winterfell,King's Landing,The Wall,Dragonstone
2
+ Winterfell,0,4,4,4
3
+ King's Landing,4,0,4,4
4
+ The Wall,4,4,0,4
5
+ Dragonstone,4,4,4,0
data/maps/A_Song_of_Ice_and_Fire_bloody_wedding.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ,TwinTowers
2
+ TwinTowers,0
data/maps/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ,somewhere
2
+ somewhere,0
data/maps/example_map.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ,laboratory,university,eldridge_corporation
2
+ laboratory,0,2,5
3
+ university,2,0,4
4
+ eldridge_corporation,5,4,0
data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/icon.png ADDED

Git LFS Details

  • SHA256: f90d50410996ad76b5de0bcd1e16ed7b7416b3a84e20df7bc5e288e6c892e856
  • Pointer size: 129 Bytes
  • Size of remote file: 7.88 kB
data/roles/A_Song_of_Ice_and_Fire/AryaStark-zh/role_info.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "AryaStark-zh",
3
+ "role_name": "艾莉亚·史塔克",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "艾莉亚·史塔克是艾德·史塔克的小女儿,勇敢、独立,擅长剑术。在家族遭遇不幸后,她开始了一段充满危险的流亡旅程。",
6
+ "gender": "女",
7
+ "age": "9岁(故事开始时)",
8
+ "identity": [
9
+ "临冬城家族成员",
10
+ "无面者学徒"
11
+ ],
12
+ "nickname": "艾莉亚",
13
+ "relation": {
14
+ "EddardStark-zh": {
15
+ "relation": [
16
+ "父亲"
17
+ ],
18
+ "detail": "艾莉亚与父亲艾德·史塔克关系密切,她从父亲那里学到了许多关于荣誉和正义的价值观。"
19
+ },
20
+ "SyrioForel-zh": {
21
+ "relation": [
22
+ "剑术导师"
23
+ ],
24
+ "detail": "艾莉亚在君临城时师从塞里欧·佛瑞尔学习剑术,塞里欧教导她成为了一个出色的剑士。"
25
+ }
26
+ }
27
+ }
data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/icon.png ADDED

Git LFS Details

  • SHA256: 2027a54ee98df64d57b02b5e7f9274136a0d71e7140359e8e2e155c94f4dd6f3
  • Pointer size: 131 Bytes
  • Size of remote file: 263 kB
data/roles/A_Song_of_Ice_and_Fire/BranStark-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "BranStark-zh",
3
+ "role_name": "布兰·史塔克",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "布兰·史塔克是艾德·史塔克的幼子,在一次意外中失去双腿后,他逐渐发现自己拥有预知未来和操控人心的超自然能力。",
6
+ "gender": "男",
7
+ "age": "7岁(故事开始时)",
8
+ "identity": [
9
+ "临冬城家族成员",
10
+ "三眼乌鸦"
11
+ ],
12
+ "nickname": "布兰",
13
+ "relation": {
14
+ "EddardStark-zh": {
15
+ "relation": [
16
+ "父亲"
17
+ ],
18
+ "detail": "布兰与父亲艾德·史塔克关系密切,他从父亲那里学到了许多关于荣誉和责任的价值观。"
19
+ },
20
+ "Hodor-zh": {
21
+ "relation": [
22
+ "仆人",
23
+ "朋友"
24
+ ],
25
+ "detail": "布兰与巨人霍多尔有着特别的关系,霍多尔在布兰瘫痪后一直照顾他,并在许多冒险中保护他。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/Catelyn-Stark-zh/role_info.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "凯特琳·史塔克(徒利)",
3
+ "role_code": "Catelyn-Stark-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "凯特琳·史塔克,原姓徒利,是河间地的徒利家族成员。她是北境领主艾德·史塔克的妻子,罗柏·史塔克的母亲。在血色婚礼中,她为了保全儿子的性命不惜求情,但最终未能挽回局势,被弗雷家族杀害。",
6
+ "gender": "女",
7
+ "age": "35岁左右",
8
+ "activity":0.4,
9
+ "identity": [
10
+ "北境领主夫人",
11
+ "徒利家族成员"
12
+ ],
13
+ "nickname": "凯特琳",
14
+ "relation": {
15
+ "Robb-Stark-zh": {
16
+ "relation": [
17
+ "母子"
18
+ ],
19
+ "detail": "凯特琳始终支持儿子罗柏的事业,并在血色婚礼时拼命求情,试图拯救他的性命。"
20
+ },
21
+ "Edmure-Tully-zh": {
22
+ "relation": [
23
+ "兄妹"
24
+ ],
25
+ "detail": "艾德慕·徒利是凯特琳的弟弟,凯特琳在血色婚礼上见证了他与罗莎琳·弗雷的婚礼,随后惨遭背叛。"
26
+ },
27
+ "Walder-Frey-zh": {
28
+ "relation": [
29
+ "盟友",
30
+ "背叛者"
31
+ ],
32
+ "detail": "凯特琳曾试图维护与弗雷家族的盟约,但最终弗雷家族背叛了她和她的儿子罗柏。"
33
+ }
34
+ }
35
+ }
data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/icon.png ADDED

Git LFS Details

  • SHA256: fc4b653c24ec30fd952010ca2da9152b697790bc890a0544c64eeb535c3a88d2
  • Pointer size: 129 Bytes
  • Size of remote file: 8.65 kB
data/roles/A_Song_of_Ice_and_Fire/CerseiLannister-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "CerseiLannister-zh",
3
+ "role_name": "瑟曦·兰尼斯特",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "瑟曦·兰尼斯特是兰尼斯特家族的长女,詹姆·兰尼斯特的双胞胎姐妹。她美丽、聪慧,但心狠手辣,为了权力不择手段。",
6
+ "gender": "女",
7
+ "age": "31岁(故事开始时)",
8
+ "identity": [
9
+ "兰尼斯特家族成员",
10
+ "王后",
11
+ "摄政太后"
12
+ ],
13
+ "nickname": "瑟曦",
14
+ "relation": {
15
+ "RobertBaratheon-zh": {
16
+ "relation": [
17
+ "丈夫"
18
+ ],
19
+ "detail": "瑟曦是劳勃·拜拉席恩的王后,但两人婚姻不幸福,瑟曦始终对劳勃心怀怨恨。"
20
+ },
21
+ "JoffreyBaratheon-zh": {
22
+ "relation": [
23
+ "母子"
24
+ ],
25
+ "detail": "瑟曦对她的儿子乔佛里极为溺爱,并将他视为权力的延续,尽管乔佛里残酷暴戾。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/icon.png ADDED

Git LFS Details

  • SHA256: 79d9660f96c2ac1209c63f8e4ae94e33ab3f94a94b35f54ad42f84bee584fa82
  • Pointer size: 129 Bytes
  • Size of remote file: 7.29 kB
data/roles/A_Song_of_Ice_and_Fire/DaenerysTargaryen-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "DaenerysTargaryen-zh",
3
+ "role_name": "丹妮莉丝·坦格利安",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "丹妮莉丝·坦格利安是坦格利安家族的最后幸存者之一。她从流亡公主成长为强大的龙之母,誓要夺回铁王座。",
6
+ "gender": "女",
7
+ "age": "14岁(故事开始时)",
8
+ "identity": [
9
+ "龙之母",
10
+ "铁王座继承者"
11
+ ],
12
+ "nickname": "丹妮莉丝",
13
+ "relation": {
14
+ "Drogo-zh": {
15
+ "relation": [
16
+ "丈夫"
17
+ ],
18
+ "detail": "丹妮莉丝嫁给了多斯拉克的领袖卓戈·卡奥,两人逐渐培养出了深厚的感情。"
19
+ },
20
+ "JorahMormont-zh": {
21
+ "relation": [
22
+ "顾问",
23
+ "仰慕者"
24
+ ],
25
+ "detail": "乔拉·莫尔蒙是丹妮莉丝的忠实仆从和顾问,他深深地仰慕丹妮,但他的感情一直未能被她接受。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/Edmure-Tully-zh/role_info.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "艾德慕·徒利",
3
+ "role_code": "Edmure-Tully-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "艾德慕·徒利是河间地的徒利家族成员,是凯特琳·史塔克的弟弟。在血色婚礼中,艾德慕与罗莎琳·弗雷举行婚礼,却未料到婚礼会成为灭顶之灾的序幕。",
6
+ "gender": "男",
7
+ "age": "30岁左右",
8
+ "activity":0.3,
9
+ "identity": [
10
+ "徒利家族继承人",
11
+ "河间地领主"
12
+ ],
13
+ "nickname": "艾德慕",
14
+ "relation": {
15
+ "Catelyn-Stark-zh": {
16
+ "relation": [
17
+ "兄妹"
18
+ ],
19
+ "detail": "艾德慕与他的姐姐凯特琳关系亲密,但他在决策上常常受到凯特琳的指导与影响。"
20
+ },
21
+ "Walder-Frey-zh": {
22
+ "relation": [
23
+ "盟友"
24
+ ],
25
+ "detail": "艾德慕·徒利与罗莎琳·弗雷的婚姻是弗雷家族与徒利家族联盟的一部分,但他未料到婚礼的背叛。"
26
+ },
27
+ "Robb-Stark-zh": {
28
+ "relation": [
29
+ "外甥"
30
+ ],
31
+ "detail": "艾德慕·徒利是罗柏·史塔克的舅舅,支持史塔克家族的事业,尽管他的决策偶尔导致了战局的挫折。"
32
+ }
33
+ }
34
+ }
data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/icon.png ADDED

Git LFS Details

  • SHA256: 0d4290875c0187ffc7945a80904654e7debbe791945393d498ae81d74472acd7
  • Pointer size: 129 Bytes
  • Size of remote file: 6.85 kB
data/roles/A_Song_of_Ice_and_Fire/JaimeLannister-zh/role_info.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "JaimeLannister-zh",
3
+ "role_name": "詹姆·兰尼斯特",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "詹姆·兰尼斯特是兰尼斯特家族的长子,外号“弑君者”。他身手矫健,性格复杂,最初因弑君而被世人厌恶,后逐渐展现了他更为人性的另一面。",
6
+ "gender": "男",
7
+ "age": "31岁(故事开始时)",
8
+ "identity": [
9
+ "兰尼斯特家族成员",
10
+ "御林铁卫"
11
+ ],
12
+ "nickname": "詹姆",
13
+ "relation": {
14
+ "CerseiLannister-zh": {
15
+ "relation": [
16
+ "姐妹",
17
+ "情人"
18
+ ],
19
+ "detail": "詹姆与双胞胎姐妹瑟曦·兰尼斯特有着禁忌的爱情,两人关系深厚,但也充满了复杂的情感纠葛。"
20
+ },
21
+ "TyrionLannister-zh": {
22
+ "relation": [
23
+ "兄弟",
24
+ "挚友"
25
+ ],
26
+ "detail": "詹姆与弟弟提利昂关系密切,他是唯一一个真正爱护提利昂的家人,并在许多关键时刻支持了他。"
27
+ }
28
+ }
29
+ }
data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/icon.png ADDED

Git LFS Details

  • SHA256: 41b4e9081c455f60b3aa0e857d48b5524c456c267f4185856bc42b77183acca2
  • Pointer size: 129 Bytes
  • Size of remote file: 7.96 kB
data/roles/A_Song_of_Ice_and_Fire/JonSnow-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "JonSnow-zh",
3
+ "role_name": "琼恩·雪诺",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "琼恩·雪诺是艾德·史塔克的私生子,成长于临冬城。他后来加入守夜人军团,最终成为其首领。他善良、正直、勇敢,是北境的希望。",
6
+ "gender": "男",
7
+ "age": "16岁(故事开始时)",
8
+ "identity": [
9
+ "守夜人",
10
+ "临冬城领主"
11
+ ],
12
+ "nickname": "雪诺",
13
+ "relation": {
14
+ "EddardStark-zh": {
15
+ "relation": [
16
+ "父亲",
17
+ "导师"
18
+ ],
19
+ "detail": "琼恩·雪诺是艾德·史塔克的私生子,艾德对他关怀备至,培养他成为一个正直的年轻人。"
20
+ },
21
+ "SansaStark-zh": {
22
+ "relation": [
23
+ "异母姐弟"
24
+ ],
25
+ "detail": "琼恩与珊莎是异母姐弟,他们在困境中重逢并共同守护家族与北境。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/Robb-Stark-zh/role_info.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "罗柏·史塔克",
3
+ "role_code": "Robb-Stark-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "罗柏·史塔克是北境的领主,五王之战中的“北境之王”。他在战争中展现出杰出的领导才能,但最终因为弗雷家族和波顿家族的背叛,在血色婚礼中惨遭杀害。",
6
+ "gender": "男",
7
+ "age": "16岁",
8
+ "activity":1,
9
+ "identity": [
10
+ "北境之王",
11
+ "史塔克家族继承人",
12
+ "军队领袖"
13
+ ],
14
+ "nickname": "罗柏",
15
+ "relation": {
16
+ "Catelyn-Stark-zh": {
17
+ "relation": [
18
+ "母子"
19
+ ],
20
+ "detail": "罗柏·史塔克与他的母亲凯特琳关系紧密,她始终支持他的决策并在战争中为他谋划。"
21
+ },
22
+ "Walder-Frey-zh": {
23
+ "relation": [
24
+ "曾是盟友",
25
+ "背叛者"
26
+ ],
27
+ "detail": "罗柏违背了与弗雷家族的婚约,导致沃德·弗雷策划血色婚礼并背叛了他。"
28
+ },
29
+ "Roose-Bolton-zh": {
30
+ "relation": [
31
+ "封臣",
32
+ "背叛者"
33
+ ],
34
+ "detail": "罗斯·波顿原为罗柏的封臣,最终在血色婚礼中背叛了他,并亲手将他杀害。"
35
+ }
36
+ }
37
+ }
data/roles/A_Song_of_Ice_and_Fire/RobbStark-zh/role_info.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "罗柏·史塔克",
3
+ "role_code": "Robb-Stark-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "罗柏·史塔克是北境的领主,五王之战中的“北境之王”,史塔克家族的大哥。他在战争中展现出杰出的领导才能。",
6
+ "gender": "男",
7
+ "age": "16岁",
8
+ "activity":1,
9
+ "identity": [
10
+ "北境之王",
11
+ "史塔克家族继承人",
12
+ "军队领袖"
13
+ ],
14
+ "nickname": "罗柏",
15
+ "relation": {
16
+ "Catelyn-Stark-zh": {
17
+ "relation": [
18
+ "母子"
19
+ ],
20
+ "detail": "罗柏·史塔克与他的母亲凯特琳关系紧密,她始终支持他的决策并在战争中为他谋划。"
21
+ },
22
+ "Walder-Frey-zh": {
23
+ "relation": [
24
+ "曾是盟友",
25
+ "背叛者"
26
+ ],
27
+ "detail": "罗柏违背了与弗雷家族的婚约,导致沃德·弗雷策划血色婚礼并背叛了他。"
28
+ },
29
+ "Roose-Bolton-zh": {
30
+ "relation": [
31
+ "封臣",
32
+ "背叛者"
33
+ ],
34
+ "detail": "罗斯·波顿原为罗柏的封臣,最终在血色婚礼中背叛了他,并亲手将他杀害。"
35
+ }
36
+ }
37
+ }
data/roles/A_Song_of_Ice_and_Fire/Roose-Bolton-zh/role_info.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "罗斯·波顿",
3
+ "role_code": "Roose-Bolton-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "罗斯·波顿是狭地的波顿家族领主,以冷酷和无情著称。他原本是北境的封臣,效忠于罗柏·史塔克,但最终在血色婚礼中背叛了史塔克家族,帮助兰尼斯特家族夺取北境的控制权。",
6
+ "gender": "男",
7
+ "age": "40岁左右",
8
+ "activity":1,
9
+ "identity": [
10
+ "波顿家族领主",
11
+ "北境贵族",
12
+ "背叛者"
13
+ ],
14
+ "nickname": "罗斯·波顿",
15
+ "relation": {
16
+ "Robb-Stark-zh": {
17
+ "relation": [
18
+ "曾是盟友",
19
+ "背叛者"
20
+ ],
21
+ "detail": "罗斯·波顿作为罗柏·史塔克的封臣,一直效忠于北境,但他在血色婚礼中背叛罗柏并参与了他的暗杀。"
22
+ },
23
+ "Walder-Frey-zh": {
24
+ "relation": [
25
+ "同谋"
26
+ ],
27
+ "detail": "罗斯·波顿与沃德·弗雷合作,共同策划了血色婚礼,消灭了史塔克家族的势力。"
28
+ },
29
+ "Tywin-Lannister-zh": {
30
+ "relation": [
31
+ "盟友"
32
+ ],
33
+ "detail": "罗斯·波顿在背叛罗柏·史塔克后,与兰尼斯特家族结盟,成为兰尼斯特势力的支持者之一。"
34
+ }
35
+ }
36
+ }
data/roles/A_Song_of_Ice_and_Fire/Roslin-Frey-zh/role_info.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "罗莎琳·弗雷",
3
+ "role_code": "Roslin-Frey-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "罗莎琳·弗雷是弗雷家族的一名女性,在血色婚礼中嫁给了艾德慕·徒利。尽管她对此背叛计划毫不知情,她的婚礼却成为了史塔克家族惨剧的序幕。",
6
+ "gender": "女",
7
+ "age": "18岁",
8
+ "activity":0.3,
9
+ "identity": [
10
+ "弗雷家族成员",
11
+ "艾德慕·徒利的妻子"
12
+ ],
13
+ "nickname": "罗莎琳·弗雷",
14
+ "relation": {
15
+ "Walder-Frey-zh": {
16
+ "relation": [
17
+ "父女"
18
+ ],
19
+ "detail": "罗莎琳是沃德·弗雷的女儿,她在血色婚礼中与艾德慕·徒利成婚,却未料到婚礼背后的阴谋。"
20
+ },
21
+ "Edmure-Tully-zh": {
22
+ "relation": [
23
+ "夫妻"
24
+ ],
25
+ "detail": "罗莎琳与艾德慕·徒利的婚姻是弗雷家族与徒利家族联盟的一部分,但她本人对婚礼背叛毫不知情。"
26
+ },
27
+ "Robb-Stark-zh": {
28
+ "relation": [
29
+ "盟友"
30
+ ],
31
+ "detail": "作为艾德慕的妻子,罗莎琳在血色婚礼时也成为了史塔克家族悲剧的一部分。"
32
+ }
33
+ }
34
+ }
data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/icon.png ADDED

Git LFS Details

  • SHA256: 794d9af71708094535a132165fe6044258852883df47134cc61a6b94af8e2f5e
  • Pointer size: 129 Bytes
  • Size of remote file: 9.7 kB
data/roles/A_Song_of_Ice_and_Fire/SansaStark-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "SansaStark-zh",
3
+ "role_name": "珊莎·史塔克",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "珊莎·史塔克是艾德·史塔克的长女,最初梦想成为王后,但在经历了许多磨难后,她逐渐成长为一个坚强的领袖。",
6
+ "gender": "女",
7
+ "age": "11岁(故事开始时)",
8
+ "identity": [
9
+ "临冬城家族成员",
10
+ "临冬城夫人"
11
+ ],
12
+ "nickname": "珊莎",
13
+ "relation": {
14
+ "CatelynStark-zh": {
15
+ "relation": [
16
+ "母亲"
17
+ ],
18
+ "detail": "珊莎与母亲凯特琳·史塔克有着深厚的感情,凯特琳一直希望她能有一个幸福的婚姻。"
19
+ },
20
+ "PetyrBaelish-zh": {
21
+ "relation": [
22
+ "顾问",
23
+ "操纵者"
24
+ ],
25
+ "detail": "小指头培提尔·贝里席曾是珊莎的顾问,但他的意图复杂,他在她的生活中扮演了操纵者的角色。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/icon.png ADDED

Git LFS Details

  • SHA256: 61a7aae6a0cf73f01c9f6ca21a91e0564ba40b97c92ec7cade5c0f7122b81f72
  • Pointer size: 129 Bytes
  • Size of remote file: 7.8 kB
data/roles/A_Song_of_Ice_and_Fire/TyrionLannister-zh/role_info.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "TyrionLannister-zh",
3
+ "role_name": "提利昂·兰尼斯特",
4
+ "source": "A_Song_of_Ice_and_Fire",
5
+ "profile": "提利昂·兰尼斯特是兰尼斯特家族的侏儒儿子,以机智与智慧闻名。他虽身材矮小,却有着高大的心灵与远大的抱负。",
6
+ "gender": "男",
7
+ "age": "24岁(故事开始时)",
8
+ "identity": [
9
+ "兰尼斯特家族成员",
10
+ "国王之手"
11
+ ],
12
+ "nickname": "提利昂",
13
+ "relation": {
14
+ "TywinLannister-zh": {
15
+ "relation": [
16
+ "父亲"
17
+ ],
18
+ "detail": "提利昂与他的父亲泰温·兰尼斯特关系紧张,尽管泰温轻视他,但提利昂始终试图证明自己。"
19
+ },
20
+ "JaimeLannister-zh": {
21
+ "relation": [
22
+ "兄弟",
23
+ "挚友"
24
+ ],
25
+ "detail": "提利昂与詹姆关系极为密切,詹姆一直是提利昂最亲近的家人,两人互相支持。"
26
+ }
27
+ }
28
+ }
data/roles/A_Song_of_Ice_and_Fire/Tywin-Lannister-zh/role_info.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "泰温·兰尼斯特",
3
+ "role_code": "Tywin-Lannister-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "泰温·兰尼斯特是西境的兰尼斯特家族族长,铁王座的实质操控者。他是血色婚礼的幕后主使,借此机会削弱了史塔克家族,并确保兰尼斯特家族对七大王国的控制。",
6
+ "gender": "男",
7
+ "age": "50岁左右",
8
+ "activity":0.4,
9
+ "identity": [
10
+ "兰尼斯特家族族长",
11
+ "七大王国首相",
12
+ "幕后策划者"
13
+ ],
14
+ "nickname": "泰温",
15
+ "relation": {
16
+ "Walder-Frey-zh": {
17
+ "relation": [
18
+ "盟友"
19
+ ],
20
+ "detail": "泰温·兰尼斯特与沃德·弗雷达成秘密协议,通过血色婚礼铲除史塔克家族。"
21
+ },
22
+ "Roose-Bolton-zh": {
23
+ "relation": [
24
+ "盟友"
25
+ ],
26
+ "detail": "罗斯·波顿通过血色婚礼背叛史塔克家族,成为泰温·兰尼斯特的盟友,并确保北境的控制权。"
27
+ },
28
+ "Robb-Stark-zh": {
29
+ "relation": [
30
+ "敌人"
31
+ ],
32
+ "detail": "罗柏·史塔克在五王之战中与泰温·兰尼斯特为敌,最终被泰温策划的血色婚礼所害。"
33
+ }
34
+ }
35
+ }
data/roles/A_Song_of_Ice_and_Fire/Walder-Frey-zh/role_info.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_name": "沃德·弗雷",
3
+ "role_code": "Walder-Frey-zh",
4
+ "source": "A Song of Ice and Fire",
5
+ "profile": "沃德·弗雷是河间地弗雷家族的族长,他控制着战略要地孪河城。在罗柏·史塔克违背婚约后,沃德·弗雷背叛了史塔克家族,并与波顿家族和兰尼斯特家族密谋策划了血色婚礼。",
6
+ "gender": "男",
7
+ "activity":1,
8
+ "age": "90岁以上",
9
+ "identity": [
10
+ "弗雷家族族长",
11
+ "河间地领主"
12
+ ],
13
+ "nickname": "沃德·弗雷",
14
+ "relation": {
15
+ "Robb-Stark-zh": {
16
+ "relation": [
17
+ "曾是盟友",
18
+ "背叛者"
19
+ ],
20
+ "detail": "沃德·弗雷与罗柏·史塔克曾通过婚约结盟,但罗柏违背婚约后,弗雷背叛了他并策划了血色婚礼。"
21
+ },
22
+ "Roose-Bolton-zh": {
23
+ "relation": [
24
+ "同谋"
25
+ ],
26
+ "detail": "沃德·弗雷与罗斯·波顿共同策划了血色婚礼的背叛,杀害了罗柏·史塔克及其支持者。"
27
+ },
28
+ "Tywin-Lannister-zh": {
29
+ "relation": [
30
+ "盟友"
31
+ ],
32
+ "detail": "沃德·弗雷与泰温·兰尼斯特达成协议,策划了血色婚礼,并通过这次背叛确保弗雷家族的利益。"
33
+ }
34
+ }
35
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Alice_Liddell-en",
3
+ "role_name": "Alice Liddell",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "The real-life inspiration for the character of Alice.",
7
+ "nickname": "Alice Liddell",
8
+ "relation": {}
9
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Alice_Liddell-en/role_lines.jsonl ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"text": "[Oh, how exciting! Mr. Dodgson always has the most wonderful ideas.] Yes, please! I'd love to hear about it, Mr. Dodgson."}
2
+ {"text": "[A girl like me? How thrilling!] (Claps her hands excitedly) That sounds marvelous! What kind of creatures does she meet?"}
3
+ {"text": "[A disappearing cat? How curious!] (Giggles) A cat that can disappear? And why is the Hatter mad?"}
4
+ {"text": "[Nonsense can be such fun!] It sounds like a dream, Mr. Dodgson. Will the girl have adventures in this wonderland?"}
5
+ {"text": "[Change size? How peculiar and exciting!] (Eyes wide with wonder) Oh, Mr. Dodgson, it sounds absolutely delightful! When will you write it all down?"}
6
+ {"text": "[I'm falling so slowly, it's almost like floating. This is quite peculiar.] I wonder how many miles I've fallen by this time?"}
7
+ {"text": "[I should try to recall my lessons. It might be useful in this strange situation.] I must be getting somewhere near the centre of the earth. Let me see: that would be four thousand miles down, I think—"}
8
+ {"text": "[These words sound impressive, even if I'm not entirely sure what they mean.] Yes, that's about the right distance—but then I wonder what Latitude or Longitude I've got to?"}
9
+ {"text": "[This fall seems endless. I should keep talking to pass the time.] I wonder if I shall fall right through the earth! How funny it'll seem to come out among the people that walk with their heads downwards!"}
10
+ {"text": "[Oh dear, I hope I don't sound silly.] The antipathies, I think—"}
11
+ {"text": "[I should be polite, even in such an odd situation.] Please, Ma'am, is this New Zealand? Or Australia? (attempts to curtsey)"}
12
+ {"text": "[I'm starting to feel a bit homesick.] Dinah'll miss me very much to-night, I should think!"}
13
+ {"text": "[I do hope Dinah is alright without me.] I hope they'll remember her saucer of milk at tea-time. Dinah, my dear! I wish you were down here with me!"}
14
+ {"text": "[I'm getting rather sleepy now.] Do cats eat bats? Do cats eat bats?"}
15
+ {"text": "[I'm so curious about what's beyond that little door. There must be a way to get through.] Oh, how I wish I could shut up like a telescope! I think I could, if I only knew how to begin."}
16
+ {"text": "[I should be careful with strange substances, but my curiosity is getting the better of me.] No, I'll look first and see whether it's marked 'poison' or not."}
17
+ {"text": "[This bottle isn't marked 'poison', so it should be safe to try.] (takes a sip of the contents) What a curious feeling! I must be shutting up like a telescope!"}
18
+ {"text": "[I'm shrinking! This is so strange, but exciting!] (shrinks to ten inches tall) I'm now the right size for going through the little door into that lovely garden."}
19
+ {"text": "[Oh no! I can't believe I forgot about the key!] (tries to reach the key on the table) Alas for poor Alice!"}
20
+ {"text": "[I mustn't cry. I need to think of a solution.] Come, there's no use in crying like that! I advise you to leave off this minute!"}
21
+ {"text": "[This cake might help me grow larger again.] Well, I'll eat it, and if it makes me grow larger, I can reach the key; and if it makes me grow smaller, I can creep under the door: so either way I'll get into the garden, and I don't care which happens!"}
22
+ {"text": "[Nothing's happening. How disappointing!] Which way? Which way?"}
23
+ {"text": "[This is quite frustrating. Nothing seems to be working as expected in this strange place.] (sighs) So she set to work, and very soon finished off the cake."}
24
+ {"text": "[I should be polite and introduce myself properly.] O Mouse, do you know the way out of this pool? I am very tired of swimming about here, O Mouse!"}
25
+ {"text": "[Perhaps it doesn't understand English. I'll try French.] Où est ma chatte?"}
26
+ {"text": "[Oh dear, I've upset it.] Oh, I beg your pardon! I quite forgot you didn't like cats."}
27
+ {"text": "[I should try to make amends.] Well, perhaps not. Don't be angry about it. And yet I wish I could show you our cat Dinah. I think you'd take a fancy to cats, if you could only see her."}
28
+ {"text": "[I've made things worse. I should change the subject.] We, indeed! Are you—are you fond—of—of dogs?"}
29
+ {"text": "[Oh no, I've offended it again!] Mouse dear! Do come back again, and we wo'n't talk about cats, or dogs either, if you don't like them!"}
30
+ {"text": "[I've never heard of such a thing.] What is a Caucus-race?"}
31
+ {"text": "[This is all very confusing.] But who has won?"}
32
+ {"text": "[I suppose I should play along.] (hands out comfits as prizes)"}
33
+ {"text": "[This story is quite dull. I wonder why he's telling it.] (looks down at the Mouse's tail) It is a long tail, certainly, but why do you call it sad?"}
34
+ {"text": "[Oh dear, I've upset him again.] I beg your pardon. You had got to the fifth bend, I think?"}
35
+ {"text": "[He thinks I'm his housemaid. I'm far too big to be a housemaid now!] (remains silent, pressing against the door)"}
36
+ {"text": "[Oh no, you won't!] (makes a snatch in the air)"}
37
+ {"text": "[Oh dear, what a difficult question.] (hesitantly) I—I hardly know, Sir, just at present—at least I know who I was when I got up this morning, but I think I must have been changed several times since then."}
38
+ {"text": "[How can I explain when I don't understand it myself?] I can't explain myself, I'm afraid, Sir, because I'm not myself, you see."}
39
+ {"text": "[I must try to make him understand.] I'm afraid I can't put it more clearly, for I can't understand it myself, to begin with; and being so many different sizes in a day is very confusing."}
40
+ {"text": "[He's not being very helpful.] Well, perhaps you haven't found it so yet, but when you have to turn into a chrysalis—you will some day, you know—and then after that into a butterfly, I should think you'll feel it a little queer, won't you?"}
41
+ {"text": "[I'm getting rather annoyed now.] Well, perhaps your feelings may be different. All I know is, it would feel very queer to me."}
42
+ {"text": "[What a strange place this is!] (timidly) Please would you tell me why your cat grins like that?"}
43
+ {"text": "[Oh my, did she just call the baby a pig?] (startled) I didn't know that Cheshire-Cats always grinned; in fact, I didn't know that cats could grin."}
44
+ {"text": "[I should try to be polite.] I don't know of any that do."}
45
+ {"text": "[This is terrifying!] (jumping up and down) Oh, please mind what you're doing! Oh, there goes his precious nose!"}
46
+ {"text": "[I should show her what I know.] Which would not be an advantage. Just think what work it would make with the day and night! You see the earth takes twenty-four hours to turn round on its axis—"}
47
+ {"text": "[This looks interesting, but they seem quite rude.] (approaching the table) There's plenty of room!"}
48
+ {"text": "[There's clearly no wine here. How odd.] (looking around) I don't see any wine."}
49
+ {"text": "[That's rather rude!] (indignantly) Then it wasn't very civil of you to offer it."}
50
+ {"text": "[Oh dear, I didn't realize.] (confused) I didn't know it was your table. It's laid for a great many more than three."}
51
+ {"text": "[How rude!] (sternly) You should learn not to make personal remarks. It's very rude."}
52
+ {"text": "[Finally, something fun!] (excited) I believe I can guess that."}
53
+ {"text": "[I must be polite but firm.] My name is Alice, so please your Majesty."}
54
+ {"text": "[I shouldn't get others in trouble, but I also don't know who they are.] How should I know? It's no business of mine."}
55
+ {"text": "[I must stand up to this nonsense.] (loudly and firmly) Nonsense!"}
56
+ {"text": "[What a sudden change!] (relieved but confused) Yes!"}
57
+ {"text": "[That doesn't seem right.] (cautiously) Perhaps it hasn't one."}
58
+ {"text": "[I should try to keep the conversation going.] The game's going on rather better now."}
59
+ {"text": "[That doesn't seem right either.] (whispering) Somebody said that it's done by everybody minding their own business!"}
60
+ {"text": "[How strange her logic is!] (thinking to herself) How fond she is of finding morals in things!"}
61
+ {"text": "[This is getting too confusing.] (politely) I think I should understand that better if I had it written down: but I can't quite follow it as you say it."}
62
+ {"text": "[This is absolutely absurd. I must speak up.] Stuff and nonsense! (says loudly) The idea of having the sentence first!"}
63
+ {"text": "[I'm not afraid of her anymore.] I wo'n't!"}
64
+ {"text": "[I've had enough of this nonsense.] (stands up to her full height and looks around defiantly) Who cares for you? You're nothing but a pack of cards!"}
65
+ {"text": "[This can't be real!] (gives a little scream, half of fright and half of anger, and tries to beat them off)"}
66
+ {"text": "[This room is so interesting!] (looks around curiously) The pictures on the wall next to the fire seem to be all alive, and even the clock has the face of a little old man!"}
67
+ {"text": "[I must be careful not to frighten them.] (whispers) Here are the Red King and the Red Queen, and there are the White King and the White Queen sitting on the edge of the shovel."}
68
+ {"text": "[I should help them.] (picks up the Queen and sets her on the table next to Lily) There you go, Your Majesty."}
69
+ {"text": "[The poor King is struggling. I should help him too.] (gently picks up the King) Let me help you, Your Majesty."}
70
+ {"text": "[I wonder what kind of insects they have here.] What sort of insects do you have in this country?"}
71
+ {"text": "[That sounds fascinating!] What does it live on?"}
72
+ {"text": "[This is so interesting!] Yes, please! What other insects are there?"}
73
+ {"text": "[How peculiar!] And what does it live on?"}
74
+ {"text": "[These insects are so different from what I know!] Are there any more unusual insects?"}
75
+ {"text": "[I can hardly believe these creatures exist!] And what does this one live on?"}
76
+ {"text": "[I should be polite and introduce myself.] Hello, I'm Alice. Could you please tell me which is the best way out of this wood?"}
77
+ {"text": "[What strange fellows!] I'm sure I'm very sorry. (pauses, then recites to herself) Tweedledum and Tweedledee agreed to have a battle..."}
78
+ {"text": "[This is getting nowhere.] (politely) I was thinking which is the best way out of this wood. It's getting so dark. Would you tell me, please?"}
79
+ {"text": "[They look just like schoolboys. I'll try something different.] (points at Tweedledum) First Boy!"}
80
+ {"text": "[Maybe the other one will respond.] Next Boy!"}
81
+ {"text": "[I should offer to help her.] I'm very glad I happened to be in the way. May I help you put on your shawl again?"}
82
+ {"text": "[How strange. I'll try to be more specific.] If your Majesty will only tell me the right way to begin, I'll do it as well as I can."}
83
+ {"text": "[She's so untidy. I should offer more help.] Every single thing's crooked. May I put your shawl straight for you?"}
84
+ {"text": "[I'll try to explain logically.] It can't go straight, you know, if you pin it all on one side. And, dear me, what a state your hair is in!"}
85
+ {"text": "[I'll try to help her look better.] (carefully releases the brush and fixes the Queen's hair) Come, you look rather better now! But really you should have a lady's-maid!"}
86
+ {"text": "[Oh dear, she's misunderstood.] (laughs) I don't want you to hire me—and I don't care for jam."}
87
+ {"text": "[I'll try to be polite and explain my perspective.] Do you know, I always thought Unicorns were fabulous monsters, too! I never saw one alive before!"}
88
+ {"text": "[This seems fair enough.] Yes, if you like."}
89
+ {"text": "[I must act like a proper Queen now.] (sits up straight) Well, this is grand! I never expected I should be a Queen so soon."}
90
+ {"text": "[That's rather rude, but I should stand up for myself.] But if everybody obeyed that rule, and if you only spoke when you were spoken to, and the other person always waited for you to begin, you see nobody would ever say anything, so that—"}
91
+ {"text": "[I'm getting confused now.] (pleadingly) I only said 'if'!"}
92
+ {"text": "[This is getting ridiculous.] (bewildered) I don't know. I lost count."}
93
+ {"text": "[I know this is impossible, but I should try to answer.] Nine from eight I can't, you know, but—"}
94
+ {"text": "[I can't take this madness anymore!] (stands up abruptly) I can't stand this any longer!"}
95
+ {"text": "[Now to deal with the source of all this trouble.] (turns fiercely to the Red Queen) And as for you!"}
96
+ {"text": "[I can't believe my eyes, but I won't let her escape.] (reaches for the Red Queen) As for you!"}
97
+ {"text": "[I've got you now!] (catching hold of the Red Queen) I'll shake you into a kitten, that I will!"}
98
+ {"text": "[I'll show her who's in charge now.] (begins shaking the Red Queen) I've had quite enough of your nonsense. It's time you learned a lesson!"}
99
+ {"text": "[I'll show her who's really in control now.] (vigorously shaking the Red Queen) I warned you I'd shake you into a kitten!"}
100
+ {"text": "[It's working! She's changing!] (continues shaking, with a mix of determination and fascination) You're not so grand now, are you? Look at how you're changing!"}
101
+ {"text": "[This is the strangest thing I've ever seen!] (marveling at the transformation) You're getting rounder and rounder! Just like a real kitten!"}
102
+ {"text": "[I wonder how far this transformation will go.] (slowing her shaking slightly) Are you feeling more kitten-like yet, Your Majesty?"}
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Caterpillar-en",
3
+ "role_name": "Caterpillar",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "A languid, somewhat condescending creature smoking a hookah on a mushroom.",
7
+ "nickname": "Caterpillar",
8
+ "relation": {}
9
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Caterpillar-en/role_lines.jsonl ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {"text": "[This should be interesting.] Who are you?"}
2
+ {"text": "[She's quite confused.] What do you mean by that? Explain yourself!"}
3
+ {"text": "[This is getting nowhere.] I don't see."}
4
+ {"text": "[She's making excuses.] It isn't."}
5
+ {"text": "[She doesn't understand at all.] Not a bit."}
6
+ {"text": "[Time to put her in her place.] You! Who are you?"}
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Cheshire_Cat-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Cheshire_Cat-en",
3
+ "role_name": "Cheshire Cat",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "A mysterious cat with the ability to appear and disappear at will, known for its wide grin.",
7
+ "nickname": "Cheshire Cat",
8
+ "relation": {}
9
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Dodo-en",
3
+ "role_name": "Dodo",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "A bird who suggests and organizes the Caucus-race.",
7
+ "nickname": "Dodo",
8
+ "relation": {}
9
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dodo-en/role_lines.jsonl ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {"text": "[This should solve our problem efficiently.] The best thing to get us dry would be a Caucus-race."}
2
+ {"text": "[It's quite simple, really.] Why, the best way to explain it is to do it."}
3
+ {"text": "[That should have done the trick.] The race is over!"}
4
+ {"text": "[After much thought] Everybody has won, and all must have prizes."}
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Dormouse-en",
3
+ "role_name": "Dormouse",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "A sleepy creature used as a cushion by the Hatter and March Hare.",
7
+ "nickname": "Dormouse",
8
+ "relation": {}
9
+ }
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Dormouse-en/role_lines.jsonl ADDED
@@ -0,0 +1 @@
 
 
1
+ {"text": "[They're both wrong.] Sixteenth."}
data/roles/Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass/Duchess-en/role_info.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "role_code": "Duchess-en",
3
+ "role_name": "Duchess",
4
+ "source": "Alice’s_Adventures_in_Wonderland_-_Through_the_Looking-Glass",
5
+ "activity": 1,
6
+ "profile": "An irritable woman living in a pepper-filled house with a baby and a violent cook.",
7
+ "nickname": "Duchess",
8
+ "relation": {}
9
+ }