feat: kickoff prose parser via classifier
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""Kickoff prose parser.
|
||||
|
||||
Service-layer function that converts a bot's authored kickoff prose into a
|
||||
structured ``KickoffParse`` for the kickoff confirm-and-edit step (T13 will
|
||||
wire this into the UI flow).
|
||||
|
||||
The classifier prompt includes only the bot context that's load-bearing for
|
||||
parsing the opening scene: name, persona, the authored
|
||||
``initial_relationship_to_you`` blurb, the ``you`` entity name, and the
|
||||
kickoff prose itself. Other identity fields (traits, backstory, ...) are
|
||||
intentionally left out — they would be noise for this extraction.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from chat.llm.classify import classify
|
||||
from chat.llm.client import LLMClient
|
||||
|
||||
|
||||
class ActivityShape(BaseModel):
|
||||
"""Per-entity activity at scene start.
|
||||
|
||||
Maps onto Requirements §6.5: ``current_action.{verb,interruptible,
|
||||
required_attention,expected_duration}`` plus posture, attention, holding.
|
||||
``action_required_attention`` is left as a free-form string ("low" /
|
||||
"medium" / "high" expected) rather than a Literal so the classifier has
|
||||
room to vary phrasing in v1.
|
||||
"""
|
||||
|
||||
posture: str
|
||||
action_verb: str
|
||||
action_interruptible: bool
|
||||
action_required_attention: str # low | medium | high
|
||||
action_expected_duration: str
|
||||
attention: str = ""
|
||||
holding: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class KickoffParse(BaseModel):
|
||||
"""Structured opening-scene state extracted from kickoff prose.
|
||||
|
||||
``container_properties`` is loose ``dict``: the classifier may emit
|
||||
``moving`` / ``public`` / ``audible_range`` keys, but downstream
|
||||
consumers (T13's confirm form) handle missing keys gracefully.
|
||||
``initial_time_iso`` is stored as text — not validated as a datetime
|
||||
here; ``chat_state.time`` stores it as a plain string.
|
||||
"""
|
||||
|
||||
container_name: str
|
||||
container_type: str
|
||||
container_properties: dict
|
||||
you_activity: ActivityShape
|
||||
bot_activity: ActivityShape
|
||||
initial_time_iso: str
|
||||
edge_seed_summary: str
|
||||
edge_seed_knowledge_facts: list[str]
|
||||
|
||||
|
||||
_SYSTEM_PROMPT = (
|
||||
"You are extracting structured scene state from a roleplay kickoff "
|
||||
"scene description. The user provides bot context and a prose "
|
||||
"description of the opening scene; you output JSON conforming to the "
|
||||
"schema. Be concrete: pick a single container, single activity per "
|
||||
"entity, and a sensible initial in-fiction time. Anything not stated "
|
||||
"explicitly should be inferred reasonably from the prose."
|
||||
)
|
||||
|
||||
|
||||
def _build_user_prompt(
|
||||
*,
|
||||
bot_name: str,
|
||||
bot_persona: str,
|
||||
initial_relationship_to_you: str,
|
||||
kickoff_prose: str,
|
||||
you_name: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"BOT NAME: {bot_name}\n"
|
||||
f"BOT PERSONA: {bot_persona}\n"
|
||||
f"INITIAL RELATIONSHIP TO {you_name}: {initial_relationship_to_you}\n"
|
||||
f"YOU NAME: {you_name}\n"
|
||||
f"KICKOFF PROSE:\n{kickoff_prose}"
|
||||
)
|
||||
|
||||
|
||||
async def parse_kickoff(
|
||||
client: LLMClient,
|
||||
*,
|
||||
model: str,
|
||||
bot_name: str,
|
||||
bot_persona: str,
|
||||
initial_relationship_to_you: str,
|
||||
kickoff_prose: str,
|
||||
you_name: str,
|
||||
timeout_s: float = 10.0,
|
||||
) -> KickoffParse:
|
||||
"""Parse authored kickoff prose into a structured ``KickoffParse``.
|
||||
|
||||
Internally calls :func:`chat.llm.classify.classify` with a labeled
|
||||
user prompt. Raises ``RuntimeError`` if the classifier fails twice in
|
||||
a row — no default is supplied at this layer, since the caller (T13's
|
||||
confirm form) is responsible for showing an error and letting the
|
||||
user edit.
|
||||
"""
|
||||
user_prompt = _build_user_prompt(
|
||||
bot_name=bot_name,
|
||||
bot_persona=bot_persona,
|
||||
initial_relationship_to_you=initial_relationship_to_you,
|
||||
kickoff_prose=kickoff_prose,
|
||||
you_name=you_name,
|
||||
)
|
||||
return await classify(
|
||||
client,
|
||||
model=model,
|
||||
system=_SYSTEM_PROMPT,
|
||||
user=user_prompt,
|
||||
schema=KickoffParse,
|
||||
timeout_s=timeout_s,
|
||||
)
|
||||
Reference in New Issue
Block a user