Skip to content

Commit 6051d53

Browse files
committed
feat: en version
1 parent fb44e05 commit 6051d53

File tree

14 files changed

+1266
-207
lines changed

14 files changed

+1266
-207
lines changed

demos/AOTAI_Hike/backend/aotai_hike/adapters/companion.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@
88

99
from aotai_hike.adapters.memory import MemoryNamespace, MemOSMemoryClient
1010
from aotai_hike.schemas import Message, Role, WorldState
11-
from aotai_hike.world.map_data import AoTaiGraph
11+
from aotai_hike.theme import (
12+
_lang,
13+
_theme,
14+
prompt_night_vote_intro,
15+
prompt_night_vote_output_requirement,
16+
prompt_night_vote_query,
17+
prompt_section_candidates,
18+
prompt_section_dialogue,
19+
prompt_section_memories,
20+
prompt_story_context_line,
21+
)
22+
from aotai_hike.world.map_data import get_graph
1223

1324

1425
@dataclass
@@ -314,23 +325,18 @@ def leader_vote(
314325

315326
memories_block = "\n".join(f"- {m}" for m in memories[:8]) if memories else "(暂无)"
316327

328+
lang = _lang(world_state)
317329
system_prompt = (
318-
"你正在参与鳌太线徒步剧情游戏,现在是夜晚,需要在队伍中选出一位今晚的队长。\n"
319-
"你将扮演当前说话的队员,根据每个人的性格、人设、最近状态和对话,做出理性但有主观色彩的选择。\n\n"
320-
"【候选人列表】\n"
330+
prompt_night_vote_intro(lang) + f"{prompt_section_candidates(lang)}\n"
321331
f"{candidates_block}\n\n"
322-
"【你的记忆片段】\n"
332+
f"{prompt_section_memories(lang)}\n"
323333
f"{memories_block}\n\n"
324-
"【最近对话】\n"
334+
f"{prompt_section_dialogue(lang)}\n"
325335
f"{dialogue_block}\n\n"
326-
"【输出要求】\n"
327-
"1. 只能从候选人列表中的 id 里选择一位作为队长。\n"
328-
"2. 请输出一个 JSON 对象,格式严格为:\n"
329-
'{"vote_role_id": "<候选人id>", "reason": "<不超过40字的中文理由>"}\n'
330-
"3. 不要输出任何多余文字,不要加注释,不要加前后缀。\n"
336+
f"{prompt_night_vote_output_requirement(lang)}"
331337
)
332338

333-
vote_query = f"你是队员「{voter.name}」,请在候选人中选出今晚的队长,并给出一句理由。"
339+
vote_query = prompt_night_vote_query(lang, voter.name)
334340

335341
raw = self._memory.chat_complete(
336342
user_id=voter.role_id,
@@ -406,7 +412,7 @@ def _format_round_memory(
406412
return "本轮无NPC发言。"
407413

408414
try:
409-
node = AoTaiGraph.get_node(world_state.current_node_id)
415+
node = get_graph(_theme(world_state)).get_node(world_state.current_node_id)
410416
location_name = node.name if node else world_state.current_node_id
411417
except Exception:
412418
location_name = world_state.current_node_id
@@ -437,7 +443,7 @@ def _build_system_prompt(
437443
) -> str:
438444
# Get current node name + altitude + terrain hint
439445
try:
440-
node = AoTaiGraph.get_node(world_state.current_node_id)
446+
node = get_graph(_theme(world_state)).get_node(world_state.current_node_id)
441447
location_name = node.name if node else world_state.current_node_id
442448
altitude = (
443449
f"{getattr(node, 'altitude_m', '未知')}m"
@@ -511,7 +517,7 @@ def _build_system_prompt(
511517

512518
def _label_node(nid: str) -> str:
513519
try:
514-
n = AoTaiGraph.get_node(nid)
520+
n = get_graph(_theme(world_state)).get_node(nid)
515521
base = n.name
516522
except Exception:
517523
base = nid
@@ -543,7 +549,7 @@ def _format_route_node(nid: str) -> str:
543549
f"{npc_info_section}\n"
544550
"</你的信息介绍>\n\n"
545551
"<故事背景>\n"
546-
f"你们是一支徒步队伍,正在穿越危险的鳌太线。今天是第{world_state.day}天,当前时间是{world_state.time_of_day},天气:{world_state.weather}\n"
552+
f"{prompt_story_context_line(_lang(world_state), world_state.day, world_state.time_of_day, world_state.weather)}"
547553
f"你们现在位于:{location_name}{altitude}),地形提示:{terrain_hint or '无特别提示'}\n"
548554
f"当前队长是:{leader_name}\n"
549555
f"玩家当前扮演的角色是:{active_name}\n"

demos/AOTAI_Hike/backend/aotai_hike/router.py

Lines changed: 136 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
SessionNewRequest,
2525
SessionNewResponse,
2626
SetActiveRoleRequest,
27+
SetSessionLangRequest,
28+
SetSessionThemeRequest,
2729
ShareImageData,
2830
WorldState,
2931
)
3032
from aotai_hike.services.game_service import GameService
3133
from aotai_hike.stores.session_store import InMemorySessionStore
3234
from aotai_hike.utils.share_image import generate_share_image
33-
from aotai_hike.world.map_data import AoTaiGraph
35+
from aotai_hike.world.map_data import get_graph
3436

3537

3638
router = APIRouter(prefix="/api/demo/ao-tai", tags=["AoTai Demo"])
@@ -44,47 +46,124 @@
4446
background=_background,
4547
)
4648

47-
# Default 3 roles (server-owned config; frontend should not hardcode)
48-
_DEFAULT_ROLES: list[dict] = [
49+
# Default 3 roles per theme (server-owned config). One map = one set of default personas.
50+
# Each set has zh and en versions (name + persona).
51+
52+
# 鳌太线 (AoTai): 阿鳌 / 太白 / 小山 — zh & en persona
53+
_AOTAI_ROLES: list[dict] = [
4954
{
5055
"name": "阿鳌",
5156
"avatar_key": "green",
52-
"persona": "阿鳌:持灯的领路者,熟知鳌太古道与太白山脉。谨慎、稳重,誓要带队抵达太白之巅。",
57+
"persona_zh": "阿鳌:持灯的领路者,熟知鳌太古道与太白山脉。谨慎、稳重,誓要带队抵达太白之巅。",
58+
"persona_en": "Ao: The guide with the lamp. Knows the AoTai trail and Taibai range. Cautious and steady, determined to lead the team to the summit.",
5359
"attrs": {
54-
"stamina": 85, # High stamina - experienced leader
55-
"mood": 65, # Moderate mood - steady and reliable
56-
"experience": 45, # High experience - knows the route well
57-
"risk_tolerance": 30, # Low risk tolerance - cautious leader
58-
"supplies": 85, # Well-prepared with supplies
60+
"stamina": 85,
61+
"mood": 65,
62+
"experience": 45,
63+
"risk_tolerance": 30,
64+
"supplies": 85,
5965
},
6066
},
6167
{
6268
"name": "太白",
6369
"avatar_key": "blue",
64-
"persona": '太白:表面是器材与数据的虔信者,经验丰厚、言辞克制。暗闻2800下撤口藏有金矿,欲借"体力不支"脱队潜行。',
70+
"persona_zh": '太白:表面是器材与数据的虔信者,经验丰厚、言辞克制。暗闻2800下撤口藏有金矿,欲借"体力不支"脱队潜行。',
71+
"persona_en": "Taibai: On the surface a believer in gear and data, experienced and reserved. Secretly heard of a stash at the evacuation point and plans to slip away by feigning exhaustion.",
6572
"attrs": {
66-
"stamina": 60, # Lower stamina - may feign exhaustion
67-
"mood": 70, # Higher mood - confident and composed
68-
"experience": 50, # Very high experience - veteran hiker
69-
"risk_tolerance": 55, # Moderate risk tolerance - calculated risks
70-
"supplies": 75, # Moderate supplies
73+
"stamina": 60,
74+
"mood": 70,
75+
"experience": 50,
76+
"risk_tolerance": 55,
77+
"supplies": 75,
7178
},
7279
},
7380
{
7481
"name": "小山",
7582
"avatar_key": "red",
76-
"persona": "小山:笑容背后的新人徒步者,乐观只是外壳。多年前真主在2800下撤口埋下金矿,此行只为取回;若同伴相助便分金,不助则将其永远留在此地。",
83+
"persona_zh": "小山:笑容背后的新人徒步者,乐观只是外壳。多年前真主在2800下撤口埋下金矿,此行只为取回;若同伴相助便分金,不助则将其永远留在此地。",
84+
"persona_en": "Xiaoshan: A newcomer behind the smile; optimism is just a shell. Years ago something was left at the evacuation point; this trek is to retrieve it. Help and share; refuse and be left behind forever.",
85+
"attrs": {
86+
"stamina": 75,
87+
"mood": 80,
88+
"experience": 15,
89+
"risk_tolerance": 75,
90+
"supplies": 70,
91+
},
92+
},
93+
]
94+
95+
# 乞力马扎罗 (Kilimanjaro): 利奥/Leo, 山姆/Sam, 杰德/Jade — zh & en name + persona
96+
_KILI_ROLES: list[dict] = [
97+
{
98+
"name_zh": "利奥",
99+
"name_en": "Leo",
100+
"avatar_key": "green",
101+
"persona_zh": "利奥:持灯的领队,熟悉乞力马扎罗路线与高山环境。谨慎稳重,立志带队登顶。",
102+
"persona_en": "Leo: The guide with the lamp. Knows the Kilimanjaro routes and the mountain. Cautious and steady, determined to lead the team to the summit.",
103+
"attrs": {
104+
"stamina": 85,
105+
"mood": 65,
106+
"experience": 45,
107+
"risk_tolerance": 30,
108+
"supplies": 85,
109+
},
110+
},
111+
{
112+
"name_zh": "山姆",
113+
"name_en": "Sam",
114+
"avatar_key": "blue",
115+
"persona_zh": "山姆:表面信奉装备与数据,经验丰富、言辞克制。暗中听说某下撤点有藏物,欲借「体力不支」脱队独行。",
116+
"persona_en": "Sam: On the surface a believer in gear and data, experienced and reserved. Secretly heard of a stash at the evacuation point and plans to slip away by feigning exhaustion.",
117+
"attrs": {
118+
"stamina": 60,
119+
"mood": 70,
120+
"experience": 50,
121+
"risk_tolerance": 55,
122+
"supplies": 75,
123+
},
124+
},
125+
{
126+
"name_zh": "杰德",
127+
"name_en": "Jade",
128+
"avatar_key": "red",
129+
"persona_zh": "杰德:笑容背后的新人,乐观只是外壳。多年前在某下撤点留下东西,此行要取回;相助则分享,不助则永留此地。",
130+
"persona_en": "Jade: A newcomer behind the smile; optimism is just a shell. Years ago something was left at the evacuation point; this trek is to retrieve it. Help and share; refuse and be left behind forever.",
77131
"attrs": {
78-
"stamina": 75, # Good stamina - young and energetic
79-
"mood": 80, # Very high mood - optimistic facade
80-
"experience": 15, # Low experience - newcomer
81-
"risk_tolerance": 75, # High risk tolerance - willing to take chances
82-
"supplies": 70, # Lower supplies - less prepared
132+
"stamina": 75,
133+
"mood": 80,
134+
"experience": 15,
135+
"risk_tolerance": 75,
136+
"supplies": 70,
83137
},
84138
},
85139
]
86140

87141

142+
def _get_default_roles(theme: str | None, lang: str | None) -> list[dict]:
143+
"""One map = one set of default roles. theme=kili -> Kilimanjaro set; theme=aotai -> AoTai set. Persona by lang."""
144+
if theme == "kili":
145+
use_zh = lang == "zh"
146+
return [
147+
{
148+
"name": r["name_zh"] if use_zh else r["name_en"],
149+
"avatar_key": r["avatar_key"],
150+
"persona": r["persona_zh"] if use_zh else r["persona_en"],
151+
"attrs": r["attrs"],
152+
}
153+
for r in _KILI_ROLES
154+
]
155+
use_zh = lang != "en"
156+
return [
157+
{
158+
"name": r["name"],
159+
"avatar_key": r["avatar_key"],
160+
"persona": r["persona_zh"] if use_zh else r["persona_en"],
161+
"attrs": r["attrs"],
162+
}
163+
for r in _AOTAI_ROLES
164+
]
165+
166+
88167
def _get_ws(session_id: str) -> WorldState:
89168
ws = _sessions.get(session_id)
90169
if ws is None:
@@ -93,10 +172,9 @@ def _get_ws(session_id: str) -> WorldState:
93172

94173

95174
@router.get("/map", response_model=MapResponse)
96-
def get_map():
97-
return MapResponse(
98-
start_node_id=AoTaiGraph.start_node_id, nodes=AoTaiGraph.nodes(), edges=AoTaiGraph.edges()
99-
)
175+
def get_map(theme: str | None = None):
176+
graph = get_graph(theme)
177+
return MapResponse(start_node_id=graph.start_node_id, nodes=graph.nodes(), edges=graph.edges())
100178

101179

102180
@router.get("/background/{scene_id}", response_model=BackgroundAsset)
@@ -106,7 +184,7 @@ def get_background(scene_id: str):
106184

107185
@router.post("/session/new", response_model=SessionNewResponse)
108186
def session_new(req: SessionNewRequest):
109-
ws = _sessions.new_session(user_id=req.user_id)
187+
ws = _sessions.new_session(user_id=req.user_id, lang=req.lang, theme=req.theme)
110188
_sessions.save(ws)
111189
return SessionNewResponse(session_id=ws.session_id, world_state=ws)
112190

@@ -116,6 +194,22 @@ def get_session(session_id: str):
116194
return _get_ws(session_id)
117195

118196

197+
@router.put("/session/lang", response_model=WorldState)
198+
def set_session_lang(req: SetSessionLangRequest):
199+
ws = _get_ws(req.session_id)
200+
ws.lang = req.lang
201+
_sessions.save(ws)
202+
return ws
203+
204+
205+
@router.put("/session/theme", response_model=WorldState)
206+
def set_session_theme(req: SetSessionThemeRequest):
207+
ws = _get_ws(req.session_id)
208+
ws.theme = req.theme
209+
_sessions.save(ws)
210+
return ws
211+
212+
119213
@router.post("/roles/upsert", response_model=RoleUpsertResponse)
120214
def roles_upsert(req: RoleUpsertRequest):
121215
ws = _get_ws(req.session_id)
@@ -127,7 +221,11 @@ def roles_upsert(req: RoleUpsertRequest):
127221
if is_new_role:
128222
role = req.role
129223
cube_id = MemoryNamespace.role_cube_id(user_id=role.role_id, role_id=role.role_id)
130-
intro = f"角色名:{role.name}。人设:{role.persona or '暂无设定'}。"
224+
intro = (
225+
f"Role: {role.name}. Persona: {role.persona or 'No setting'}."
226+
if ws.lang == "en"
227+
else f"角色名:{role.name}。人设:{role.persona or '暂无设定'}。"
228+
)
131229
_memory_client.add_memory(
132230
user_id=role.role_id,
133231
cube_id=cube_id,
@@ -162,9 +260,10 @@ def roles_quickstart(req: RolesQuickstartRequest):
162260

163261
# If not overwriting, only add defaults that don't already exist by name.
164262
existing_names = {r.name for r in ws.roles}
263+
default_roles = _get_default_roles(ws.theme, ws.lang)
165264

166265
new_roles: list[Role] = []
167-
for tmpl in _DEFAULT_ROLES:
266+
for tmpl in default_roles:
168267
if not req.overwrite and tmpl["name"] in existing_names:
169268
continue
170269
# Get attrs from template - _DEFAULT_ROLES should always have "attrs" key with a dict
@@ -207,7 +306,11 @@ def roles_quickstart(req: RolesQuickstartRequest):
207306

208307
for role in new_roles:
209308
cube_id = MemoryNamespace.role_cube_id(user_id=role.role_id, role_id=role.role_id)
210-
intro = f"角色名:{role.name}。人设:{role.persona or '暂无设定'}。"
309+
intro = (
310+
f"Role: {role.name}. Persona: {role.persona or 'No setting'}."
311+
if ws.lang == "en"
312+
else f"角色名:{role.name}。人设:{role.persona or '暂无设定'}。"
313+
)
211314
_memory_client.add_memory(
212315
user_id=role.role_id,
213316
cube_id=cube_id,
@@ -347,8 +450,9 @@ def get_test_share_image():
347450
"""
348451
from aotai_hike.schemas import Role, RoleAttrs
349452
from aotai_hike.utils.share_image import GameOutcome, ShareImageGenerator
350-
from aotai_hike.world.map_data import AoTaiGraph
453+
from aotai_hike.world.map_data import get_graph
351454

455+
graph = get_graph("zh") # Test uses AoTai map
352456
# Create mock world state with finished game
353457
roles = [
354458
Role(
@@ -408,14 +512,14 @@ def get_test_share_image():
408512
for i in range(len(visited_nodes) - 1):
409513
from_id = visited_nodes[i]
410514
to_id = visited_nodes[i + 1]
411-
edges = AoTaiGraph.outgoing(from_id)
515+
edges = graph.outgoing(from_id)
412516
for edge in edges:
413517
if edge.to_node_id == to_id:
414518
total_distance += getattr(edge, "distance_km", 1.0)
415519
break
416520

417521
try:
418-
current_node = AoTaiGraph.get_node("end_exit")
522+
current_node = graph.get_node("end_exit")
419523
current_node_name = current_node.name
420524
except Exception:
421525
current_node_name = "end_exit"

demos/AOTAI_Hike/backend/aotai_hike/schemas.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ class WorldStats(BaseModel):
9090
class WorldState(BaseModel):
9191
session_id: str
9292
user_id: str
93+
lang: Literal["zh", "en"] = "zh"
94+
theme: Literal["aotai", "kili"] = "aotai"
9395

9496
active_role_id: str | None = None
9597
roles: list[Role] = Field(default_factory=list)
@@ -153,6 +155,8 @@ class MapResponse(BaseModel):
153155
class SessionNewRequest(BaseModel):
154156
user_id: str = "demo_user"
155157
seed: int | None = None
158+
lang: Literal["zh", "en"] | None = None
159+
theme: Literal["aotai", "kili"] | None = None
156160

157161

158162
class SessionNewResponse(BaseModel):
@@ -183,6 +187,16 @@ class SetActiveRoleRequest(BaseModel):
183187
active_role_id: str
184188

185189

190+
class SetSessionLangRequest(BaseModel):
191+
session_id: str
192+
lang: Literal["zh", "en"]
193+
194+
195+
class SetSessionThemeRequest(BaseModel):
196+
session_id: str
197+
theme: Literal["aotai", "kili"]
198+
199+
186200
class ActRequest(BaseModel):
187201
session_id: str
188202
action: ActionType

0 commit comments

Comments
 (0)