mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/agent+config: configurable compact prompts
Add agent config compact_system_prompt, compact_user_prefix, and
compact_seed_text; wire through summarize and seed messages. Fix default
user prefix to include {transcript}; use safe seed substitution so braces
in summaries do not break formatting.
This commit is contained in:
@@ -17,6 +17,11 @@ path_denylist = ["/secrets/", ".ssh/", ".gnupg/"]
|
||||
# Soft context budget in estimated tokens (~4 chars/token when API usage missing).
|
||||
max_context_tokens = 200000
|
||||
|
||||
# Optional: custom compact/summarisation prompts (empty = built-in defaults).
|
||||
# compact_system_prompt = ""
|
||||
# compact_user_prefix = "Summarize the following conversation:\n\n{transcript}"
|
||||
# compact_seed_text = "Compacted from {src}:\n\n{summary}\n\nCarry on."
|
||||
|
||||
# --- OpenAI-compatible (vLLM, LiteLLM, OpenAI, …) ---
|
||||
[providers.openai_compat]
|
||||
preset = "openai-compatible"
|
||||
|
||||
@@ -33,6 +33,13 @@ _SUMMARY_SYSTEM = (
|
||||
_SUMMARY_USER_PREFIX = (
|
||||
"Summarize the following conversation for continued agent work. "
|
||||
"Output only the summary (no preamble).\n\n--- transcript ---\n"
|
||||
"{transcript}"
|
||||
)
|
||||
|
||||
_SEED_MESSAGE_TEMPLATE = (
|
||||
"Conversation summary (compacted from {src}):\n\n"
|
||||
"{summary}\n\n"
|
||||
"Continue from this summary. Prefer not to re-ask for information already covered."
|
||||
)
|
||||
|
||||
|
||||
@@ -88,8 +95,21 @@ async def summarize_messages(
|
||||
temperature: float | None = 0.2,
|
||||
prompt_tokens_hint: int | None = None,
|
||||
sent_estimate_tokens: int | None = None,
|
||||
system_prompt: str | None = None,
|
||||
user_prefix: str | None = None,
|
||||
) -> str:
|
||||
"""Soft-compact history and ask the model for a dense summary (no tools)."""
|
||||
"""Soft-compact history and ask the model for a dense summary (no tools).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
system_prompt:
|
||||
Override for the summarizer system prompt.
|
||||
``None`` or empty string uses the built-in default.
|
||||
user_prefix:
|
||||
Override for the user-message prefix (appended before the transcript).
|
||||
``None`` or empty string uses the built-in default.
|
||||
The placeholder ``{transcript}`` is substituted with the rendered transcript.
|
||||
"""
|
||||
if not messages:
|
||||
msg = "nothing to compact"
|
||||
raise ValueError(msg)
|
||||
@@ -102,10 +122,19 @@ async def summarize_messages(
|
||||
if not transcript.strip():
|
||||
msg = "nothing to compact"
|
||||
raise ValueError(msg)
|
||||
|
||||
sys_content = system_prompt.strip() if system_prompt else _SUMMARY_SYSTEM
|
||||
user_prefix_resolved = user_prefix.strip() if user_prefix else _SUMMARY_USER_PREFIX
|
||||
if "{transcript}" in user_prefix_resolved:
|
||||
user_content = user_prefix_resolved.replace("{transcript}", transcript)
|
||||
else:
|
||||
# Custom prefix without placeholder: append transcript (legacy/default shape).
|
||||
user_content = user_prefix_resolved + transcript
|
||||
|
||||
param = ChatCompletionsParam(
|
||||
messages=[
|
||||
SystemChatMessage(content=_SUMMARY_SYSTEM),
|
||||
UserChatMessage(content=_SUMMARY_USER_PREFIX + transcript),
|
||||
SystemChatMessage(content=sys_content),
|
||||
UserChatMessage(content=user_content),
|
||||
],
|
||||
model=model,
|
||||
temperature=temperature if temperature is not None else UNSET,
|
||||
@@ -126,20 +155,26 @@ def build_compacted_seed_messages(
|
||||
*,
|
||||
system_prompt: str | None = None,
|
||||
source_session_id: int | None = None,
|
||||
seed_text: str | None = None,
|
||||
) -> list[AnyChatMessage]:
|
||||
"""Messages to seed a new session after compact.
|
||||
|
||||
Summary is an assistant message so history does not end with a user turn
|
||||
(which would look like an incomplete /retry-able request).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
seed_text:
|
||||
Override template for the seed assistant message body.
|
||||
``None`` or empty string uses the built-in default.
|
||||
Placeholders ``{src}`` and ``{summary}`` are substituted.
|
||||
"""
|
||||
out: list[AnyChatMessage] = []
|
||||
if system_prompt:
|
||||
out.append(SystemChatMessage(content=system_prompt))
|
||||
src = f"session {source_session_id}" if source_session_id is not None else "prior session"
|
||||
body = (
|
||||
f"Conversation summary (compacted from {src}):\n\n"
|
||||
f"{summary}\n\n"
|
||||
"Continue from this summary. Prefer not to re-ask for information already covered."
|
||||
)
|
||||
template = seed_text.strip() if seed_text else _SEED_MESSAGE_TEMPLATE
|
||||
# Use replace (not str.format) so braces inside the model summary are safe.
|
||||
body = template.replace("{src}", src).replace("{summary}", summary)
|
||||
out.append(AssistantChatMessage(content=body))
|
||||
return out
|
||||
|
||||
@@ -30,6 +30,11 @@ _MINIMAL_CONFIG = """\
|
||||
# confirm_destructive = true
|
||||
# path_denylist = ["/secrets/", ".ssh/"]
|
||||
# max_context_tokens = 200000
|
||||
#
|
||||
# # Optional compact prompts (empty = use built-in defaults).
|
||||
# # compact_system_prompt = ""
|
||||
# # compact_user_prefix = "Summarize:\n\n{transcript}"
|
||||
# # compact_seed_text = "Compacted from {src}:\n\n{summary}"
|
||||
|
||||
# [providers.example]
|
||||
# preset = "openai-compatible"
|
||||
|
||||
@@ -303,6 +303,8 @@ class ReplState:
|
||||
max_context_tokens=self.agent.max_context_tokens,
|
||||
prompt_tokens_hint=hint,
|
||||
sent_estimate_tokens=sent_est,
|
||||
system_prompt=self.config.agent_config.compact_system_prompt or None,
|
||||
user_prefix=self.config.agent_config.compact_user_prefix or None,
|
||||
)
|
||||
session_name = name or f"compact-from-{old_id}"
|
||||
await self.new_session(name=session_name)
|
||||
@@ -315,6 +317,7 @@ class ReplState:
|
||||
summary,
|
||||
system_prompt=self.agent.system_prompt,
|
||||
source_session_id=old_id,
|
||||
seed_text=self.config.agent_config.compact_seed_text or None,
|
||||
)
|
||||
self.agent.messages = list(seed)
|
||||
for message in seed:
|
||||
|
||||
@@ -20,6 +20,11 @@ class AgentConfig(Struct, omit_defaults=True):
|
||||
path_denylist: list[str] = field(default_factory=list)
|
||||
max_context_tokens: int = 200_000
|
||||
|
||||
# Compact / summarisation prompts (empty = use built-in defaults).
|
||||
compact_system_prompt: str = ""
|
||||
compact_user_prefix: str = ""
|
||||
compact_seed_text: str = ""
|
||||
|
||||
|
||||
class ModelConfig(Struct, omit_defaults=True):
|
||||
"""Capability flags for a model within a provider."""
|
||||
|
||||
@@ -59,6 +59,19 @@ def test_build_compacted_seed_messages() -> None:
|
||||
assert "session 3" in content
|
||||
|
||||
|
||||
def test_build_compacted_seed_custom_template_and_braces_in_summary() -> None:
|
||||
seed = build_compacted_seed_messages(
|
||||
"use {braces} carefully",
|
||||
source_session_id=1,
|
||||
seed_text="From {src}:\n{summary}",
|
||||
)
|
||||
assert isinstance(seed[0], AssistantChatMessage)
|
||||
content = seed[0].content
|
||||
assert isinstance(content, str)
|
||||
assert "session 1" in content
|
||||
assert "use {braces} carefully" in content
|
||||
|
||||
|
||||
class SummaryClient:
|
||||
last: ChatCompletionsParam | None
|
||||
|
||||
@@ -111,3 +124,27 @@ async def test_summarize_messages() -> None:
|
||||
from msgspec import UNSET
|
||||
|
||||
assert client.last.tools is UNSET
|
||||
user_msg = client.last.messages[1]
|
||||
assert isinstance(user_msg, UserChatMessage)
|
||||
assert "hello" in user_msg.content
|
||||
assert "world" in user_msg.content
|
||||
|
||||
|
||||
async def test_summarize_custom_prompts() -> None:
|
||||
client = SummaryClient()
|
||||
_ = await summarize_messages(
|
||||
client,
|
||||
[UserChatMessage(content="hello")],
|
||||
model="m",
|
||||
system_prompt="SYS-CUSTOM",
|
||||
user_prefix="PREFIX:\n{transcript}\nEND",
|
||||
)
|
||||
assert client.last is not None
|
||||
sys_msg = client.last.messages[0]
|
||||
user_msg = client.last.messages[1]
|
||||
assert isinstance(sys_msg, SystemChatMessage)
|
||||
assert sys_msg.content == "SYS-CUSTOM"
|
||||
assert isinstance(user_msg, UserChatMessage)
|
||||
assert user_msg.content.startswith("PREFIX:")
|
||||
assert "hello" in user_msg.content
|
||||
assert user_msg.content.endswith("END")
|
||||
|
||||
Reference in New Issue
Block a user