This commit is contained in:
2026-09-18 15:21:47 +08:00
parent 0d8c34a2c9
commit aa27d35a47
55 changed files with 2842 additions and 471 deletions
+62 -14
View File
@@ -10,6 +10,7 @@ from app.models.agent import (
from app.models.demographics import DemographicProfile
from app.models.world import WorldBlueprint
from app.services.llm import LLMClient, parse_json
from app.services.i18n import language_directive
logger = logging.getLogger(__name__)
@@ -242,8 +243,21 @@ Return ONLY valid JSON. No markdown, no explanation."""
class CitizenGenerator:
def __init__(self, llm: LLMClient):
def __init__(self, llm: LLMClient, locale: str = "en"):
self.llm = llm
self._locale = locale
def set_locale(self, locale: str) -> None:
self._locale = locale
@staticmethod
def _has_cjk(text: str) -> bool:
return any("\u4e00" <= ch <= "\u9fff" for ch in text)
def _directive(self, system_prompt: str) -> str:
"""Wrap a system prompt with the language directive (head + tail)."""
head, tail = language_directive(self._locale)
return head + "\n\n" + system_prompt + "\n\n" + tail
async def generate(
self,
@@ -337,7 +351,7 @@ class CitizenGenerator:
) or " (not available)"
char_line = f"CITY CHARACTER: {demographics.city_character}" if demographics.city_character else ""
system_prompt = DEMOGRAPHIC_CAST_PROMPT.format(
base = DEMOGRAPHIC_CAST_PROMPT.format(
city_name=demographics.city_name,
state=demographics.state,
population=demographics.population,
@@ -350,8 +364,9 @@ class CitizenGenerator:
eth_dist=eth_dist,
city_character=char_line,
)
system_prompt = self._directive(base)
else:
system_prompt = CAST_SYSTEM_PROMPT
system_prompt = self._directive(CAST_SYSTEM_PROMPT)
user_text = f"{world_context}\n\nGenerate exactly {count} citizens."
if proposed_change:
@@ -367,7 +382,7 @@ class CitizenGenerator:
system=system_prompt,
user=user_text,
json_mode=True,
max_tokens=3000,
max_tokens=4000,
)
data = parse_json(response)
@@ -378,17 +393,50 @@ class CitizenGenerator:
else:
cast = []
# If the user picked a Chinese locale but the LLM ignored our language
# directive and returned English names, retry once with a stronger nudge.
if (
self._locale == "zh"
and cast
and not any(self._has_cjk(str(c.get("name", ""))) for c in cast)
):
logger.warning("Cast generation returned non-CJK names despite zh locale, retrying. Names: %s", [c.get('name') for c in cast])
retry_user = user_text + (
"\n\n[STRICT REMINDER] All names, roles, personality hooks, and "
"stances MUST be written in Chinese characters. Do NOT use English."
)
response = await self.llm.generate(
system=system_prompt,
user=retry_user,
json_mode=True,
max_tokens=3000,
)
data = parse_json(response)
if isinstance(data, dict) and "citizens" in data:
cast = data["citizens"]
elif isinstance(data, list):
cast = data
if len(cast) < count:
logger.warning("Cast generation returned %d/%d citizens, padding", len(cast), count)
while len(cast) < count:
idx = len(cast)
cast.append({
"name": f"Citizen {idx + 1}",
"role": "Laborer",
"age": 30 + (idx % 40),
"personality_hook": "A quiet person trying to get by",
"stance": "Indifferent to the rules",
})
if self._locale == "zh":
cast.append({
"name": f"居民{idx + 1}号",
"role": "劳动者",
"age": 30 + (idx % 40),
"personality_hook": "一个安静过日子的人",
"stance": "对规则漠不关心",
})
else:
cast.append({
"name": f"Citizen {idx + 1}",
"role": "Laborer",
"age": 30 + (idx % 40),
"personality_hook": "A quiet person trying to get by",
"stance": "Indifferent to the rules",
})
return cast[:count]
@@ -418,7 +466,7 @@ class CitizenGenerator:
f"Personality hook: {member.get('personality_hook', '')}\n"
f"Stance on tensions: {member.get('stance', 'unknown')}"
)
prompts.append((PERSONA_SYSTEM_PROMPT, f"{world_ctx}\n\n{citizen_ctx}"))
prompts.append((self._directive(PERSONA_SYSTEM_PROMPT), f"{world_ctx}\n\n{citizen_ctx}"))
responses = await self.llm.generate_batch(prompts, json_mode=True, max_tokens=800)
@@ -482,7 +530,7 @@ class CitizenGenerator:
)
response = await self.llm.generate(
system=RELATIONSHIPS_SYSTEM_PROMPT,
system=self._directive(RELATIONSHIPS_SYSTEM_PROMPT),
user=f"World: {blueprint.name}\nRules: {'; '.join(blueprint.rules)}\n\nCitizens:\n{agent_summaries}",
json_mode=True,
max_tokens=3000,
@@ -551,7 +599,7 @@ class CitizenGenerator:
prompts = []
for agent in batch:
num_events = self._num_formative_events(agent.age)
system = LIFE_HISTORY_PROMPT.format(num_events=num_events)
system = self._directive(LIFE_HISTORY_PROMPT.format(num_events=num_events))
world_ctx = (
f"World: {blueprint.name}\n"
f"Rules: {'; '.join(blueprint.rules)}\n"