Replaces LICENSE (GPLv3 -> AGPLv3) and prepends `SPDX-License-Identifier: AGPL-3.0-or-later` to every source file across decnet/, decnet_web/, tests/, scripts/, and tools/. Rationale: closes the GPLv3 ASP loophole so any party operating a modified DECNET as a network service must offer their modified source. Personal copyright (Samuel Paschuan) + inbound=outbound contributions make a future unilateral relicense infeasible. - LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt) - COPYRIGHT: project copyright notice - tools/add_spdx_headers.py: idempotent header injector (shebang- and PEP 263-aware) Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh). No behavior change; comments only.
154 lines
4.5 KiB
Python
154 lines
4.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Prompt builder behaviour: language constraint, em-dash suppression,
|
|
deterministic mannerism injection."""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
|
|
from decnet.realism.personas import EmailPersona
|
|
from decnet.realism.prompts.email import (
|
|
PromptInputs,
|
|
build,
|
|
select_mannerisms,
|
|
)
|
|
|
|
|
|
def _persona(**over) -> EmailPersona:
|
|
base = dict(
|
|
name="John Smith",
|
|
email="john@corp.com",
|
|
role="COO",
|
|
tone="formal",
|
|
mannerisms=[
|
|
"opens with 'I hope this finds you well'",
|
|
"uses 'Best regards' exclusively",
|
|
"references policy by number",
|
|
"ccs legal",
|
|
],
|
|
language="en",
|
|
)
|
|
base.update(over)
|
|
return EmailPersona(**base)
|
|
|
|
|
|
class _SeededRng:
|
|
"""Adapter so prompt code thinks it has a SystemRandom."""
|
|
|
|
def __init__(self, seed: int):
|
|
self._r = random.Random(seed)
|
|
|
|
def shuffle(self, seq):
|
|
self._r.shuffle(seq)
|
|
|
|
def random(self):
|
|
return self._r.random()
|
|
|
|
def choice(self, seq):
|
|
return self._r.choice(seq)
|
|
|
|
|
|
def test_select_mannerisms_returns_subset_of_pool():
|
|
persona = _persona()
|
|
picks = select_mannerisms(persona, rng=_SeededRng(0), n=2)
|
|
assert len(picks) == 2
|
|
assert all(m in persona.mannerisms for m in picks)
|
|
|
|
|
|
def test_select_mannerisms_deterministic_under_same_seed():
|
|
persona = _persona()
|
|
a = select_mannerisms(persona, rng=_SeededRng(42), n=2)
|
|
b = select_mannerisms(persona, rng=_SeededRng(42), n=2)
|
|
assert a == b
|
|
|
|
|
|
def test_select_mannerisms_returns_all_when_pool_smaller_than_n():
|
|
persona = _persona(mannerisms=["a"])
|
|
picks = select_mannerisms(persona, rng=_SeededRng(0), n=2)
|
|
assert picks == ["a"]
|
|
|
|
|
|
def test_select_mannerisms_empty_pool():
|
|
persona = _persona(mannerisms=[])
|
|
assert select_mannerisms(persona) == []
|
|
|
|
|
|
def test_build_includes_language_constraint_english():
|
|
sender = _persona(language="en")
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "in English" in prompt
|
|
|
|
|
|
def test_build_includes_language_constraint_spanish():
|
|
sender = _persona(language="es")
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "in Spanish" in prompt
|
|
|
|
|
|
def test_build_em_dash_suppression_default():
|
|
sender = _persona()
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "Do NOT use em-dashes" in prompt
|
|
|
|
|
|
def test_build_em_dash_lifted_for_llm_heavy_persona():
|
|
sender = _persona(uses_llms_heavily=True)
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "Do NOT use em-dashes" not in prompt
|
|
assert "fine" in prompt.lower()
|
|
|
|
|
|
def test_build_reply_thread_block_prefixes_re():
|
|
sender = _persona()
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(
|
|
sender=sender,
|
|
recipient=recip,
|
|
context_hint="budget",
|
|
parent_subject="Re: Q3 budget",
|
|
parent_excerpt="Numbers attached.",
|
|
),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "REPLY in an ongoing thread" in prompt
|
|
assert "Re: Q3 budget" in prompt
|
|
assert "Numbers attached" in prompt
|
|
assert "prefixed with 'Re: '" in prompt
|
|
|
|
|
|
def test_build_returns_mannerisms_used_metadata():
|
|
sender = _persona()
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
_, used = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(7),
|
|
)
|
|
assert used
|
|
assert all(m in sender.mannerisms for m in used)
|
|
|
|
|
|
def test_build_uses_explicit_signature_when_provided():
|
|
sender = _persona(signature="-- John\\nCOO")
|
|
recip = _persona(name="Sarah", email="sarah@corp.com", role="PM")
|
|
prompt, _ = build(
|
|
PromptInputs(sender=sender, recipient=recip, context_hint="budget"),
|
|
rng=_SeededRng(0),
|
|
)
|
|
assert "Use this exact signature block" in prompt
|