++
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user