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).
|
# Soft context budget in estimated tokens (~4 chars/token when API usage missing).
|
||||||
max_context_tokens = 200000
|
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, …) ---
|
# --- OpenAI-compatible (vLLM, LiteLLM, OpenAI, …) ---
|
||||||
[providers.openai_compat]
|
[providers.openai_compat]
|
||||||
preset = "openai-compatible"
|
preset = "openai-compatible"
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ _SUMMARY_SYSTEM = (
|
|||||||
_SUMMARY_USER_PREFIX = (
|
_SUMMARY_USER_PREFIX = (
|
||||||
"Summarize the following conversation for continued agent work. "
|
"Summarize the following conversation for continued agent work. "
|
||||||
"Output only the summary (no preamble).\n\n--- transcript ---\n"
|
"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,
|
temperature: float | None = 0.2,
|
||||||
prompt_tokens_hint: int | None = None,
|
prompt_tokens_hint: int | None = None,
|
||||||
sent_estimate_tokens: int | None = None,
|
sent_estimate_tokens: int | None = None,
|
||||||
|
system_prompt: str | None = None,
|
||||||
|
user_prefix: str | None = None,
|
||||||
) -> str:
|
) -> 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:
|
if not messages:
|
||||||
msg = "nothing to compact"
|
msg = "nothing to compact"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
@@ -102,10 +122,19 @@ async def summarize_messages(
|
|||||||
if not transcript.strip():
|
if not transcript.strip():
|
||||||
msg = "nothing to compact"
|
msg = "nothing to compact"
|
||||||
raise ValueError(msg)
|
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(
|
param = ChatCompletionsParam(
|
||||||
messages=[
|
messages=[
|
||||||
SystemChatMessage(content=_SUMMARY_SYSTEM),
|
SystemChatMessage(content=sys_content),
|
||||||
UserChatMessage(content=_SUMMARY_USER_PREFIX + transcript),
|
UserChatMessage(content=user_content),
|
||||||
],
|
],
|
||||||
model=model,
|
model=model,
|
||||||
temperature=temperature if temperature is not None else UNSET,
|
temperature=temperature if temperature is not None else UNSET,
|
||||||
@@ -126,20 +155,26 @@ def build_compacted_seed_messages(
|
|||||||
*,
|
*,
|
||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
source_session_id: int | None = None,
|
source_session_id: int | None = None,
|
||||||
|
seed_text: str | None = None,
|
||||||
) -> list[AnyChatMessage]:
|
) -> list[AnyChatMessage]:
|
||||||
"""Messages to seed a new session after compact.
|
"""Messages to seed a new session after compact.
|
||||||
|
|
||||||
Summary is an assistant message so history does not end with a user turn
|
Summary is an assistant message so history does not end with a user turn
|
||||||
(which would look like an incomplete /retry-able request).
|
(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] = []
|
out: list[AnyChatMessage] = []
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
out.append(SystemChatMessage(content=system_prompt))
|
out.append(SystemChatMessage(content=system_prompt))
|
||||||
src = f"session {source_session_id}" if source_session_id is not None else "prior session"
|
src = f"session {source_session_id}" if source_session_id is not None else "prior session"
|
||||||
body = (
|
template = seed_text.strip() if seed_text else _SEED_MESSAGE_TEMPLATE
|
||||||
f"Conversation summary (compacted from {src}):\n\n"
|
# Use replace (not str.format) so braces inside the model summary are safe.
|
||||||
f"{summary}\n\n"
|
body = template.replace("{src}", src).replace("{summary}", summary)
|
||||||
"Continue from this summary. Prefer not to re-ask for information already covered."
|
|
||||||
)
|
|
||||||
out.append(AssistantChatMessage(content=body))
|
out.append(AssistantChatMessage(content=body))
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ _MINIMAL_CONFIG = """\
|
|||||||
# confirm_destructive = true
|
# confirm_destructive = true
|
||||||
# path_denylist = ["/secrets/", ".ssh/"]
|
# path_denylist = ["/secrets/", ".ssh/"]
|
||||||
# max_context_tokens = 200000
|
# 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]
|
# [providers.example]
|
||||||
# preset = "openai-compatible"
|
# preset = "openai-compatible"
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ class ReplState:
|
|||||||
max_context_tokens=self.agent.max_context_tokens,
|
max_context_tokens=self.agent.max_context_tokens,
|
||||||
prompt_tokens_hint=hint,
|
prompt_tokens_hint=hint,
|
||||||
sent_estimate_tokens=sent_est,
|
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}"
|
session_name = name or f"compact-from-{old_id}"
|
||||||
await self.new_session(name=session_name)
|
await self.new_session(name=session_name)
|
||||||
@@ -315,6 +317,7 @@ class ReplState:
|
|||||||
summary,
|
summary,
|
||||||
system_prompt=self.agent.system_prompt,
|
system_prompt=self.agent.system_prompt,
|
||||||
source_session_id=old_id,
|
source_session_id=old_id,
|
||||||
|
seed_text=self.config.agent_config.compact_seed_text or None,
|
||||||
)
|
)
|
||||||
self.agent.messages = list(seed)
|
self.agent.messages = list(seed)
|
||||||
for message in seed:
|
for message in seed:
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class AgentConfig(Struct, omit_defaults=True):
|
|||||||
path_denylist: list[str] = field(default_factory=list)
|
path_denylist: list[str] = field(default_factory=list)
|
||||||
max_context_tokens: int = 200_000
|
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):
|
class ModelConfig(Struct, omit_defaults=True):
|
||||||
"""Capability flags for a model within a provider."""
|
"""Capability flags for a model within a provider."""
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ def test_build_compacted_seed_messages() -> None:
|
|||||||
assert "session 3" in content
|
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:
|
class SummaryClient:
|
||||||
last: ChatCompletionsParam | None
|
last: ChatCompletionsParam | None
|
||||||
|
|
||||||
@@ -111,3 +124,27 @@ async def test_summarize_messages() -> None:
|
|||||||
from msgspec import UNSET
|
from msgspec import UNSET
|
||||||
|
|
||||||
assert client.last.tools is 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