108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
"""Parse user-supplied "have they met?" prose into per-direction seed
|
|
content for two bots' edges (T38).
|
|
|
|
Per Requirements §5.2, when two bots first co-appear in a chat, the user
|
|
is offered a small drawer asking "Have they met before? If yes, write a
|
|
short prose seed describing how." That prose lands here and is parsed
|
|
into a :class:`RelationshipSeed` whose two halves populate the
|
|
``botA -> botB`` and ``botB -> botA`` edges respectively (summary,
|
|
initial knowledge facts, and small affinity/trust deltas around the
|
|
default 50/50 baseline).
|
|
|
|
The two directions can differ — A may know more about B than B knows
|
|
about A, or A may trust B less than the reverse — so the schema carries
|
|
both halves independently.
|
|
|
|
Empty/whitespace-only prose short-circuits to a default
|
|
``RelationshipSeed`` (all zeroes, empty strings); the caller treats
|
|
that as "they haven't met" and writes no edge content. The wrapper uses
|
|
:func:`chat.llm.classify.classify` with ``default=RelationshipSeed()``
|
|
so a flapping classifier degrades to the same no-op rather than
|
|
blocking the chat-creation flow (§3.3 graceful-degradation rule).
|
|
|
|
T42 (the inter-bot relationship drawer) calls this from the route layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from chat.llm.classify import classify
|
|
from chat.llm.client import LLMClient
|
|
|
|
|
|
class RelationshipSeed(BaseModel):
|
|
"""Structured per-direction seed for two bots' edges.
|
|
|
|
Defaults are a deliberate no-op: empty summaries, empty knowledge
|
|
lists, zero deltas. Both the empty-prose short-circuit and the
|
|
classifier-failure fallback return this default so the caller can
|
|
treat them identically.
|
|
"""
|
|
|
|
a_to_b_summary: str = ""
|
|
a_to_b_knowledge_facts: list[str] = Field(default_factory=list)
|
|
a_to_b_affinity_delta: int = 0 # signed, -10..+10 typical
|
|
a_to_b_trust_delta: int = 0
|
|
b_to_a_summary: str = ""
|
|
b_to_a_knowledge_facts: list[str] = Field(default_factory=list)
|
|
b_to_a_affinity_delta: int = 0
|
|
b_to_a_trust_delta: int = 0
|
|
|
|
|
|
_SYSTEM = (
|
|
"You parse a short prose seed describing how two characters know each "
|
|
"other into structured per-direction edge content. For each direction "
|
|
"(A -> B, B -> A) extract: summary (one sentence from that POV), "
|
|
"knowledge_facts (list of factual claims that direction can carry "
|
|
"into future scenes), affinity_delta (-10..+10 — small adjustments to "
|
|
"the default 50/50 baseline), trust_delta (-10..+10). Default deltas "
|
|
"to 0 when prose is neutral. The two directions can differ — A may "
|
|
"trust B more than B trusts A. Output strict JSON matching the schema."
|
|
)
|
|
|
|
|
|
async def seed_inter_bot_edges(
|
|
client: LLMClient,
|
|
*,
|
|
classifier_model: str,
|
|
bot_a_id: str,
|
|
bot_a_name: str,
|
|
bot_b_id: str,
|
|
bot_b_name: str,
|
|
relationship_prose: str,
|
|
timeout_s: float = 30.0,
|
|
) -> RelationshipSeed:
|
|
"""Parse user-supplied prose into structured edge content for both
|
|
directed pairs.
|
|
|
|
Empty/whitespace prose short-circuits to an empty
|
|
:class:`RelationshipSeed` (the caller treats this as "they haven't
|
|
met" and writes no edge content). Classifier failure also returns
|
|
the default — see module docstring for the rationale.
|
|
|
|
The ``bot_a_id`` / ``bot_b_id`` arguments are accepted for symmetry
|
|
with the caller (T42's drawer route uses them when emitting
|
|
``edge_update`` events); they're embedded in the prompt alongside
|
|
the names so the classifier can disambiguate when names collide.
|
|
"""
|
|
if not relationship_prose or not relationship_prose.strip():
|
|
return RelationshipSeed()
|
|
user = (
|
|
f"Bot A: {bot_a_name} (id={bot_a_id})\n"
|
|
f"Bot B: {bot_b_name} (id={bot_b_id})\n\n"
|
|
f"Prose seed:\n{relationship_prose.strip()}"
|
|
)
|
|
return await classify(
|
|
client,
|
|
model=classifier_model,
|
|
system=_SYSTEM,
|
|
user=user,
|
|
schema=RelationshipSeed,
|
|
default=RelationshipSeed(),
|
|
timeout_s=timeout_s,
|
|
)
|
|
|
|
|
|
__all__ = ["RelationshipSeed", "seed_inter_bot_edges"]
|