++
This commit is contained in:
+117
-117
@@ -1,117 +1,117 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.llm import LLMClient
|
||||
from app.services.i18n import (
|
||||
locale_from_request,
|
||||
translate,
|
||||
language_directive as _language_directive,
|
||||
DEFAULT_LOCALE,
|
||||
)
|
||||
|
||||
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL)
|
||||
|
||||
|
||||
def _strip_thinking(text: str, locale: str) -> str:
|
||||
"""Strip LLM thinking blocks. The actual response content is what we
|
||||
return to the client; the model's internal reasoning is never shown.
|
||||
Falls back to a localised canned line when nothing is left after
|
||||
stripping (rare, but happens when the model burns all tokens on thinking).
|
||||
"""
|
||||
cleaned = _THINK_BLOCK.sub("", text or "").strip()
|
||||
if not cleaned:
|
||||
return {
|
||||
"en": "(no response)",
|
||||
"zh": "(没有回应)",
|
||||
}.get(locale, "(no response)")
|
||||
return cleaned
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class InterviewRequest(BaseModel):
|
||||
question: str
|
||||
|
||||
|
||||
@router.get("/simulation/{sim_id}/agents")
|
||||
async def list_agents(sim_id: str, request: Request):
|
||||
agents = await request.app.state.store.get_all_agents(sim_id)
|
||||
return {"agents": [a.model_dump() for a in agents]}
|
||||
|
||||
|
||||
@router.get("/simulation/{sim_id}/agent/{agent_id}")
|
||||
async def get_agent(sim_id: str, agent_id: int, request: Request):
|
||||
agent = await request.app.state.store.get_agent(sim_id, agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, translate("agent_not_found", locale_from_request(request)))
|
||||
return agent.model_dump()
|
||||
|
||||
|
||||
@router.post("/simulation/{sim_id}/agent/{agent_id}/interview")
|
||||
async def interview(sim_id: str, agent_id: int, req: InterviewRequest, request: Request):
|
||||
store = request.app.state.store
|
||||
llm: LLMClient = request.app.state.llm
|
||||
locale = locale_from_request(request)
|
||||
|
||||
agent = await store.get_agent(sim_id, agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, translate("agent_not_found", locale))
|
||||
|
||||
world_state = await store.get_world_state(sim_id)
|
||||
world_name = world_state.blueprint.name if world_state else "the settlement"
|
||||
|
||||
head, tail = _language_directive(locale)
|
||||
system = (
|
||||
f"{head}\n\n"
|
||||
f"You are {agent.name}, a {agent.age}-year-old {agent.role} in {world_name}.\n"
|
||||
f"Your personality: honesty={agent.personality.honesty:.1f}, "
|
||||
f"empathy={agent.personality.empathy:.1f}, "
|
||||
f"confrontational={agent.personality.confrontational:.1f}\n"
|
||||
f"Your core memories: {'; '.join(agent.core_memory[:5])}\n"
|
||||
f"Your beliefs: {'; '.join(agent.beliefs[:5])}\n"
|
||||
f"Your emotional state: {agent.emotional_state}\n\n"
|
||||
"Someone approaches and asks you a question. Respond in character. "
|
||||
"Be authentic to your personality. Reference specific events from your memory. "
|
||||
"Keep it to 2-3 sentences.\n\n"
|
||||
f"{tail}"
|
||||
)
|
||||
|
||||
if agent.life_state:
|
||||
life_lines = []
|
||||
|
||||
# Domain levels
|
||||
for domain in ["finances", "career", "health"]:
|
||||
val = getattr(agent.life_state, domain, 0.5)
|
||||
if val < 0.3:
|
||||
life_lines.append(f"Your {domain} situation is dire")
|
||||
elif val < 0.5:
|
||||
life_lines.append(f"Your {domain} is tight but you manage")
|
||||
elif val > 0.7:
|
||||
life_lines.append(f"Your {domain} is in good shape")
|
||||
|
||||
# Family
|
||||
if agent.life_state.family:
|
||||
family_strs = [f"{f.name} ({f.relation}, {f.age}, {f.status})" for f in agent.life_state.family]
|
||||
life_lines.append(f"Your family: {', '.join(family_strs)}")
|
||||
|
||||
# Pressures
|
||||
if agent.life_state.pressures:
|
||||
pressure_strs = [p.description for p in agent.life_state.pressures[:3]]
|
||||
life_lines.append(f"What weighs on you: {'; '.join(pressure_strs)}")
|
||||
|
||||
# Childhood
|
||||
if agent.life_state.childhood_summary:
|
||||
life_lines.append(f"Your upbringing: {agent.life_state.childhood_summary[:200]}")
|
||||
|
||||
if life_lines:
|
||||
system += "\n\nYOUR LIFE SITUATION:\n" + "\n".join(life_lines)
|
||||
system += "\n\nWhen answering, let your life situation color your responses naturally. Reference family, pressures, or your past when relevant."
|
||||
|
||||
response = await llm.generate(system=system, user=req.question, max_tokens=400)
|
||||
cleaned = _strip_thinking(response, locale)
|
||||
return {"response": cleaned, "emotional_state": agent.emotional_state}
|
||||
|
||||
|
||||
# moved up to module top so it can be imported by tests / shared utilities
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.llm import LLMClient
|
||||
from app.services.i18n import (
|
||||
locale_from_request,
|
||||
translate,
|
||||
language_directive as _language_directive,
|
||||
DEFAULT_LOCALE,
|
||||
)
|
||||
|
||||
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL)
|
||||
|
||||
|
||||
def _strip_thinking(text: str, locale: str) -> str:
|
||||
"""Strip LLM thinking blocks. The actual response content is what we
|
||||
return to the client; the model's internal reasoning is never shown.
|
||||
Falls back to a localised canned line when nothing is left after
|
||||
stripping (rare, but happens when the model burns all tokens on thinking).
|
||||
"""
|
||||
cleaned = _THINK_BLOCK.sub("", text or "").strip()
|
||||
if not cleaned:
|
||||
return {
|
||||
"en": "(no response)",
|
||||
"zh": "(没有回应)",
|
||||
}.get(locale, "(no response)")
|
||||
return cleaned
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class InterviewRequest(BaseModel):
|
||||
question: str
|
||||
|
||||
|
||||
@router.get("/simulation/{sim_id}/agents")
|
||||
async def list_agents(sim_id: str, request: Request):
|
||||
agents = await request.app.state.store.get_all_agents(sim_id)
|
||||
return {"agents": [a.model_dump() for a in agents]}
|
||||
|
||||
|
||||
@router.get("/simulation/{sim_id}/agent/{agent_id}")
|
||||
async def get_agent(sim_id: str, agent_id: int, request: Request):
|
||||
agent = await request.app.state.store.get_agent(sim_id, agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, translate("agent_not_found", locale_from_request(request)))
|
||||
return agent.model_dump()
|
||||
|
||||
|
||||
@router.post("/simulation/{sim_id}/agent/{agent_id}/interview")
|
||||
async def interview(sim_id: str, agent_id: int, req: InterviewRequest, request: Request):
|
||||
store = request.app.state.store
|
||||
llm: LLMClient = request.app.state.llm
|
||||
locale = locale_from_request(request)
|
||||
|
||||
agent = await store.get_agent(sim_id, agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(404, translate("agent_not_found", locale))
|
||||
|
||||
world_state = await store.get_world_state(sim_id)
|
||||
world_name = world_state.blueprint.name if world_state else "the settlement"
|
||||
|
||||
head, tail = _language_directive(locale)
|
||||
system = (
|
||||
f"{head}\n\n"
|
||||
f"You are {agent.name}, a {agent.age}-year-old {agent.role} in {world_name}.\n"
|
||||
f"Your personality: honesty={agent.personality.honesty:.1f}, "
|
||||
f"empathy={agent.personality.empathy:.1f}, "
|
||||
f"confrontational={agent.personality.confrontational:.1f}\n"
|
||||
f"Your core memories: {'; '.join(agent.core_memory[:5])}\n"
|
||||
f"Your beliefs: {'; '.join(agent.beliefs[:5])}\n"
|
||||
f"Your emotional state: {agent.emotional_state}\n\n"
|
||||
"Someone approaches and asks you a question. Respond in character. "
|
||||
"Be authentic to your personality. Reference specific events from your memory. "
|
||||
"Keep it to 2-3 sentences.\n\n"
|
||||
f"{tail}"
|
||||
)
|
||||
|
||||
if agent.life_state:
|
||||
life_lines = []
|
||||
|
||||
# Domain levels
|
||||
for domain in ["finances", "career", "health"]:
|
||||
val = getattr(agent.life_state, domain, 0.5)
|
||||
if val < 0.3:
|
||||
life_lines.append(f"Your {domain} situation is dire")
|
||||
elif val < 0.5:
|
||||
life_lines.append(f"Your {domain} is tight but you manage")
|
||||
elif val > 0.7:
|
||||
life_lines.append(f"Your {domain} is in good shape")
|
||||
|
||||
# Family
|
||||
if agent.life_state.family:
|
||||
family_strs = [f"{f.name} ({f.relation}, {f.age}, {f.status})" for f in agent.life_state.family]
|
||||
life_lines.append(f"Your family: {', '.join(family_strs)}")
|
||||
|
||||
# Pressures
|
||||
if agent.life_state.pressures:
|
||||
pressure_strs = [p.description for p in agent.life_state.pressures[:3]]
|
||||
life_lines.append(f"What weighs on you: {'; '.join(pressure_strs)}")
|
||||
|
||||
# Childhood
|
||||
if agent.life_state.childhood_summary:
|
||||
life_lines.append(f"Your upbringing: {agent.life_state.childhood_summary[:200]}")
|
||||
|
||||
if life_lines:
|
||||
system += "\n\nYOUR LIFE SITUATION:\n" + "\n".join(life_lines)
|
||||
system += "\n\nWhen answering, let your life situation color your responses naturally. Reference family, pressures, or your past when relevant."
|
||||
|
||||
response = await llm.generate(system=system, user=req.question, max_tokens=400)
|
||||
cleaned = _strip_thinking(response, locale)
|
||||
return {"response": cleaned, "emotional_state": agent.emotional_state}
|
||||
|
||||
|
||||
# moved up to module top so it can be imported by tests / shared utilities
|
||||
|
||||
+27
-27
@@ -1,27 +1,27 @@
|
||||
from __future__ import annotations
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/gallery")
|
||||
async def list_gallery(request: Request, sort: str = "recent"):
|
||||
sims = await request.app.state.store.list_simulations(public_only=True, sort=sort)
|
||||
return {"simulations": sims}
|
||||
|
||||
|
||||
@router.get("/gallery/{sim_id}")
|
||||
async def get_gallery_item(sim_id: str, request: Request):
|
||||
store = request.app.state.store
|
||||
sim = await store.get_simulation(sim_id)
|
||||
if not sim or not sim.get("is_public"):
|
||||
raise HTTPException(404, "Simulation not found in gallery")
|
||||
await store.increment_view_count(sim_id)
|
||||
cached = await store.get_report(sim_id)
|
||||
if cached and cached.get("status") == "ready":
|
||||
return cached["report"]
|
||||
narrator = request.app.state.narrator
|
||||
report = await narrator.generate_report(sim_id, store)
|
||||
if "error" not in report:
|
||||
await store.save_report(sim_id, report)
|
||||
return report
|
||||
from __future__ import annotations
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/gallery")
|
||||
async def list_gallery(request: Request, sort: str = "recent"):
|
||||
sims = await request.app.state.store.list_simulations(public_only=True, sort=sort)
|
||||
return {"simulations": sims}
|
||||
|
||||
|
||||
@router.get("/gallery/{sim_id}")
|
||||
async def get_gallery_item(sim_id: str, request: Request):
|
||||
store = request.app.state.store
|
||||
sim = await store.get_simulation(sim_id)
|
||||
if not sim or not sim.get("is_public"):
|
||||
raise HTTPException(404, "Simulation not found in gallery")
|
||||
await store.increment_view_count(sim_id)
|
||||
cached = await store.get_report(sim_id)
|
||||
if cached and cached.get("status") == "ready":
|
||||
return cached["report"]
|
||||
narrator = request.app.state.narrator
|
||||
report = await narrator.generate_report(sim_id, store)
|
||||
if "error" not in report:
|
||||
await store.save_report(sim_id, report)
|
||||
return report
|
||||
|
||||
+34
-34
@@ -1,35 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from app.presets import PRESETS
|
||||
from app.services.i18n import DEFAULT_LOCALE, locale_from_request
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
def _localize(value, locale: str):
|
||||
"""Recursively unwrap {en, zh} dicts to the requested locale.
|
||||
|
||||
Falls back to the default locale, then to the first available value.
|
||||
Strings (and other scalars) are returned as-is.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
# Heuristic: dicts with only "en"/"zh" keys are translation maps.
|
||||
keys = set(value.keys())
|
||||
if keys <= {"en", "zh"}:
|
||||
if locale in value:
|
||||
return value[locale]
|
||||
if DEFAULT_LOCALE in value:
|
||||
return value[DEFAULT_LOCALE]
|
||||
return next(iter(value.values()))
|
||||
return {k: _localize(v, locale) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_localize(v, locale) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
@router.get("/presets")
|
||||
async def get_presets(request: Request):
|
||||
locale = locale_from_request(request)
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from app.presets import PRESETS
|
||||
from app.services.i18n import DEFAULT_LOCALE, locale_from_request
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
def _localize(value, locale: str):
|
||||
"""Recursively unwrap {en, zh} dicts to the requested locale.
|
||||
|
||||
Falls back to the default locale, then to the first available value.
|
||||
Strings (and other scalars) are returned as-is.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
# Heuristic: dicts with only "en"/"zh" keys are translation maps.
|
||||
keys = set(value.keys())
|
||||
if keys <= {"en", "zh"}:
|
||||
if locale in value:
|
||||
return value[locale]
|
||||
if DEFAULT_LOCALE in value:
|
||||
return value[DEFAULT_LOCALE]
|
||||
return next(iter(value.values()))
|
||||
return {k: _localize(v, locale) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_localize(v, locale) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
@router.get("/presets")
|
||||
async def get_presets(request: Request):
|
||||
locale = locale_from_request(request)
|
||||
return {"presets": _localize(PRESETS, locale)}
|
||||
+548
-548
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user