feat(realism): LLM enrichment for user-class file bodies
Stage 6 of the realism migration. User-class file bodies (note, todo, draft, script) optionally get LLM-authored content; system classes (cron / daemon logs, /tmp caches) stay template-only because formulaic *is* the right look for them. New surface: - realism.llm.circuit.LLMCircuitBreaker — process-local sliding-window breaker. 3 consecutive failures trip open; 60s cooldown to half-open; half-open success closes, failure re-opens. Protects the orchestrator tick from sustained Ollama wedges (per-call timeout already covers one-shot hangs). - realism.prompts._style — em-dash suppression lifted from the email prompt. Persona.uses_llms_heavily opts out per the feedback_em_dash_llm_tell.md memory. Includes strip_em_dashes belt-and-braces sub for output that slipped past the prompt rule. - realism.prompts.filebody — class-conditioned prompts (note / todo / draft / script) with persona context, language pinning, output shape rule. - realism.bodies.make_body_with_llm — async wrapper around make_body that calls the LLM when one is provided AND the breaker allows. Falls back to template on timeout / error / empty / system-class. Wiring: - scheduler.pick_file accepts optional llm + llm_breaker + llm_timeout. When the planner picks a create action and the content_class is a user-class, the body_hint is replaced with the LLM-authored body (or falls back to the deterministic body_hint). - orchestrator.worker constructs get_llm() at startup gated by DECNET_REALISM_LLM env var (any non-empty value enables; empty / "off" / "none" / "0" disables). Passes llm + breaker through every tick. - decnet orchestrate gains --llm/--no-llm flag overriding the env var.
This commit is contained in:
39
decnet/realism/prompts/_style.py
Normal file
39
decnet/realism/prompts/_style.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Shared stylometric guards for LLM-bound prompts.
|
||||
|
||||
Lifted from the original ``orchestrator.emailgen.prompt`` em-dash
|
||||
block so file-class prompts (note / todo / draft / script bodies)
|
||||
pick up the same suppression. Per the
|
||||
``feedback_em_dash_llm_tell.md`` memory: em-dashes (—) are a strong
|
||||
LLM-authorship tell, suppress by default; allow only for personas
|
||||
explicitly opted in via ``EmailPersona.uses_llms_heavily``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decnet.realism.personas import EmailPersona
|
||||
|
||||
|
||||
_SUPPRESS_RULE = (
|
||||
"Do NOT use em-dashes (—). Use commas, periods, or "
|
||||
"parentheses instead. Em-dashes are a tell."
|
||||
)
|
||||
_ALLOW_RULE = (
|
||||
"Em-dashes are fine — this persona uses them naturally. "
|
||||
"Write in your usual style."
|
||||
)
|
||||
|
||||
|
||||
def em_dash_rule(persona: EmailPersona) -> str:
|
||||
"""Return the em-dash instruction line for *persona*'s prompt."""
|
||||
if persona.uses_llms_heavily:
|
||||
return _ALLOW_RULE
|
||||
return _SUPPRESS_RULE
|
||||
|
||||
|
||||
def strip_em_dashes(text: str, persona: EmailPersona) -> str:
|
||||
"""Belt-and-braces: even with the prompt rule, small models leak
|
||||
em-dashes occasionally. Substitute with comma+space so the
|
||||
output reads naturally; opt-in personas pass through unchanged.
|
||||
"""
|
||||
if persona.uses_llms_heavily:
|
||||
return text
|
||||
return text.replace("—", ", ").replace("–", ", ")
|
||||
91
decnet/realism/prompts/filebody.py
Normal file
91
decnet/realism/prompts/filebody.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Class-conditioned prompt builder for user-class file bodies.
|
||||
|
||||
Stage 6 of the realism migration. Only user-classes (``note``,
|
||||
``todo``, ``draft``, ``script``) get LLM enrichment — system-class
|
||||
content (cron logs, daemon logs, /tmp caches) is *supposed* to look
|
||||
formulaic, and an LLM-authored cron log is more suspicious than a
|
||||
templated one.
|
||||
|
||||
The prompt asks for *short* output (LLM-authored ten-page essays in
|
||||
``~/notes.txt`` are an instant tell) and pins the exit shape so the
|
||||
worker doesn't need to scrape boilerplate. Em-dash suppression
|
||||
flows through :mod:`decnet.realism.prompts._style`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decnet.realism.personas import EmailPersona
|
||||
from decnet.realism.prompts._style import em_dash_rule
|
||||
from decnet.realism.taxonomy import ContentClass
|
||||
|
||||
|
||||
_LANGUAGE_NAMES = {
|
||||
"en": "English", "es": "Spanish", "pt": "Portuguese",
|
||||
"fr": "French", "de": "German", "it": "Italian",
|
||||
"nl": "Dutch", "ja": "Japanese", "zh": "Chinese",
|
||||
}
|
||||
|
||||
|
||||
def _lang_label(code: str) -> str:
|
||||
return _LANGUAGE_NAMES.get((code or "en").lower(), code or "English")
|
||||
|
||||
|
||||
_CLASS_GUIDANCE: dict[ContentClass, str] = {
|
||||
ContentClass.NOTE: (
|
||||
"A personal note file the persona keeps on their dev box. "
|
||||
"2–6 short lines. Mix of TODOs, half-formed thoughts, "
|
||||
"shorthand reminders. NOT a polished document. No headers "
|
||||
"or markdown sections."
|
||||
),
|
||||
ContentClass.TODO: (
|
||||
"A markdown TODO list the persona keeps on their dev box. "
|
||||
"3–8 items in `- [ ] item` / `- [x] item` form. Some checked, "
|
||||
"some not. Items are short, work-flavoured, lowercase, no "
|
||||
"prose paragraphs. No headers. No introductory sentence."
|
||||
),
|
||||
ContentClass.DRAFT: (
|
||||
"A short draft email or memo the persona is working on. "
|
||||
"2–4 short paragraphs, conversational tone. No subject line, "
|
||||
"no headers — this is the body in a notes file, not a sent "
|
||||
"email. Sign off the way the persona would in their voice."
|
||||
),
|
||||
ContentClass.SCRIPT: (
|
||||
"A short utility script the persona wrote. Pick a plausible "
|
||||
"interpreter (bash or python3) and start with the matching "
|
||||
"shebang. 10–25 lines. Real-feeling intent (a backup, a "
|
||||
"log rotation, a cleanup). Inline comments allowed but sparse."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build(
|
||||
content_class: ContentClass,
|
||||
persona: EmailPersona,
|
||||
) -> str:
|
||||
"""Return a prompt for one body of *content_class* by *persona*.
|
||||
|
||||
Output the LLM is expected to produce: *just the file body*, no
|
||||
commentary, no markdown fences. Caller substitutes em-dashes
|
||||
server-side via :func:`decnet.realism.prompts._style.strip_em_dashes`
|
||||
as a belt-and-braces guard.
|
||||
"""
|
||||
guidance = _CLASS_GUIDANCE.get(content_class)
|
||||
if guidance is None:
|
||||
raise KeyError(
|
||||
f"no filebody prompt registered for content_class={content_class!r}"
|
||||
)
|
||||
language = _lang_label(persona.language or "en")
|
||||
return (
|
||||
f"You are writing one short file the persona below would "
|
||||
f"plausibly keep on their dev box.\n\n"
|
||||
f"Persona:\n"
|
||||
f"- Name: {persona.name}\n"
|
||||
f"- Role: {persona.role}\n"
|
||||
f"- Tone: {persona.tone_custom if persona.tone == 'custom' and persona.tone_custom else persona.tone}\n\n"
|
||||
f"File class: {content_class.value}\n"
|
||||
f"Guidance: {guidance}\n\n"
|
||||
f"Hard rules:\n"
|
||||
f"1. Write the file body in {language}. Do not translate or code-switch.\n"
|
||||
f"2. {em_dash_rule(persona)}\n"
|
||||
f"3. Output ONLY the file body. No commentary, no markdown "
|
||||
f" fences, no preamble like 'Here is the file:'.\n"
|
||||
).strip()
|
||||
Reference in New Issue
Block a user