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
+37 -5
View File
@@ -1,8 +1,32 @@
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")
@@ -21,7 +45,7 @@ async def list_agents(sim_id: str, request: Request):
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, "Agent not found")
raise HTTPException(404, translate("agent_not_found", locale_from_request(request)))
return agent.model_dump()
@@ -29,15 +53,18 @@ async def get_agent(sim_id: str, agent_id: int, request: Request):
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, "Agent not found")
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}, "
@@ -47,7 +74,8 @@ async def interview(sim_id: str, agent_id: int, req: InterviewRequest, request:
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."
"Keep it to 2-3 sentences.\n\n"
f"{tail}"
)
if agent.life_state:
@@ -81,5 +109,9 @@ async def interview(sim_id: str, agent_id: int, req: InterviewRequest, request:
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=200)
return {"response": response, "emotional_state": agent.emotional_state}
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