refactor revision rewrite

This commit is contained in:
vegu-ai-tools
2026-04-09 00:06:20 +03:00
parent b53c8eaaf0
commit 66b32c651c
13 changed files with 705 additions and 204 deletions

View File

@@ -38,3 +38,4 @@
- "Client Config UX: Moved advanced settings (Inference Presets, Structured Data Format, Section Format, Response Length Enforcement, Prompt Caching, Rate Limit) into a dedicated Advanced tab to declutter the general view. A convenient link in the general tab provides quick access. Toggling Simple View on now resets to the General tab, and switching clients always starts on General."
- "Prompts Tab Alert: The top-level Prompts tab now shows a warning icon when any active template overrides are outdated, so you can spot them without opening the tab."
- "Action Confirm Timeout: The director chat action confirmation timeout is now configurable (060 minutes, default 3). Set to 0 to wait indefinitely. Found in the Director Chat settings under Action Confirmation."
- "Rewrite Revision: Collapsed the targeted rewrite method from two prompts into one. The analysis and rewrite are now produced in a single LLM call with the final revision extracted from a <REVISION> tag, halving latency and token cost. Added a length guardrail that discards rewrites substantially longer than the original (mirroring the unslop safeguard), and a dynamic response length budget sized to the draft."

View File

@@ -361,9 +361,16 @@ Handlers can edit `response` in-place to clean up or transform the text (the Edi
---
### agent.editor.revision-analysis.before
Emitted **before** the Editor agent requests the revision-analysis prompt.
Emitted **before** the Editor agent requests the revision-rewrite prompt.
Handlers can add extra analysis instructions via `dynamic_instructions` or adjust `template_vars`.
!!! note
The signal is named `revision-analysis.*` for historical reasons — it was
introduced when analysis and rewrite were two separate prompts. They are
now combined into a single prompt (`editor.revision-rewrite`), but the
signal name is kept for backward compatibility.
!!! payload "Payload"
| Field | Type | Notes |
@@ -376,8 +383,18 @@ Handlers can add extra analysis instructions via `dynamic_instructions` or adjus
### agent.editor.revision-analysis.after
Emitted after the revision-analysis prompt returns but **before** the rewrite is requested.
Handlers may inspect or replace the `response` string.
Emitted after the revision-rewrite prompt returns. Intended as a
notification hook for observers of the rewrite flow.
!!! warning
Historically this signal fired between a separate analysis prompt and a
rewrite prompt, with `response` carrying the raw analysis text that
handlers could mutate before the rewrite ran. Analysis and rewrite are
now combined into a single prompt, so there is no separable "analysis
text" — `response` is not set on the emission and any mutation is
discarded. Use [`agent.editor.revision-revise.after`](#agenteditorrevision-reviseafter)
if you need to inspect or replace the final rewritten text.
!!! payload "Payload"
@@ -385,7 +402,6 @@ Handlers may inspect or replace the `response` string.
|-------|------|-------|
| `agent` | `EditorAgent` | The agent instance |
| `template_vars` | `dict` | Same vars used for the prompt |
| `response` | `str` | **Mutable.** Raw analysis text returned by the model |
## Narrator Agent Events

View File

@@ -42,7 +42,6 @@ from talemate.util import count_tokens
from talemate.prompts import Prompt
from talemate.prompts.response import ResponseSpec, AnchorExtractor
from talemate.exceptions import GenerationCancelled
import talemate.game.focal as focal
from talemate.status import LoadingStatus
from talemate.world_state.templates.content import PhraseDetection
from contextvars import ContextVar
@@ -61,6 +60,13 @@ FIX_SPEC = ResponseSpec(
required=[], # Not required - we handle None case
)
REWRITE_SPEC = ResponseSpec(
extractors={
"revision": AnchorExtractor(left="<REVISION>", right="</REVISION>"),
},
required=[], # Not required - we handle None case
)
## CONFIG CONDITIONALS
dedupe_condition = AgentActionConditional(
@@ -182,6 +188,26 @@ class RevisionEmission(AgentTemplateEmission):
# the fix as hallucinated content. Unslop should trim, not expand.
UNSLOP_MAX_LENGTH_RATIO = 1.25
# Maximum ratio of revision length to original length before discarding
# the rewrite as hallucinated content. Rewrites should stay close to the
# original length — the analysis template explicitly forbids expansion.
REWRITE_MAX_LENGTH_RATIO = 1.25
# Extra token headroom added on top of the draft's token count when sizing
# the response budget for revision prompts. Gives the model room for the
# analysis preamble before it emits the wrapped rewrite.
REVISION_RESPONSE_HEADROOM = 768
def _format_length_ratio(new_len: int, original_len: int) -> str:
"""
Format a length ratio for log output, guarding against division by zero
when the original text is empty.
"""
if not original_len:
return "inf"
return f"{new_len / original_len:.2f}"
## MIXIN
@@ -266,7 +292,7 @@ class RevisionMixin:
),
"rewrite": AgentActionNote(
color="primary",
text="Each generation will be checked for repetition and unwanted prose. If issues are found, a rewrite of the problematic part(s) will be attempted. (+2 prompts)",
text="Each generation will be checked for repetition and unwanted prose. If issues are found, a rewrite of the problematic part(s) will be attempted. (+1 prompt)",
),
},
),
@@ -950,7 +976,12 @@ class RevisionMixin:
info: RevisionInformation,
) -> str:
"""
Revise the text by rewriting
Revise the text by rewriting.
Runs a single analysis+rewrite prompt and extracts the rewritten text
from a ``<REVISION>...</REVISION>`` anchor. If the anchor is missing,
or the rewrite balloons past ``REWRITE_MAX_LENGTH_RATIO``, the original
text is returned unchanged.
"""
text = info.text
@@ -966,9 +997,6 @@ class RevisionMixin:
issues = await self.revision_collect_issues(text, character)
if loading_status:
loading_status.max_steps = 2
num_issues = len(issues.log)
if not num_issues:
@@ -994,13 +1022,13 @@ class RevisionMixin:
)
return original_text
# Step 4 - Rewrite
token_count = count_tokens(text)
response_length = token_count + REVISION_RESPONSE_HEADROOM
log.debug("revision_rewrite: token_count", token_count=token_count)
if loading_status:
loading_status("Editor - Issues identified, analyzing text...")
loading_status("Editor - Issues identified, rewriting text...")
emission = RevisionEmission(
agent=self,
@@ -1012,7 +1040,7 @@ class RevisionMixin:
"text": text,
"character": character,
"scene": self.scene,
"response_length": token_count,
"response_length": response_length,
"max_tokens": self.client.max_token_length,
"repetition": issues.repetition,
"bad_prose": issues.bad_prose,
@@ -1024,52 +1052,39 @@ class RevisionMixin:
await async_signals.get("agent.editor.revision-revise.before").send(emission)
await async_signals.get("agent.editor.revision-analysis.before").send(emission)
analysis, extracted = await Prompt.request(
"editor.revision-analysis",
_, extracted = await Prompt.request(
"editor.revision-rewrite",
self.client,
"edit_768",
f"edit_{response_length}",
vars=emission.template_vars,
dedupe_enabled=False,
response_spec=REWRITE_SPEC,
)
async def rewrite_text(text: str) -> str:
return text
analysis = extracted["response"]
emission.response = analysis
# The analysis-after signal is fired as a notification hook for
# backward compatibility. Analysis and rewrite now share a single
# prompt, so there is no separable analysis text to expose on the
# emission — listeners that previously mutated `emission.response`
# here will have no effect. See docs/user-guide/node-editor/reference/events.md.
await async_signals.get("agent.editor.revision-analysis.after").send(emission)
analysis = emission.response
focal_handler = focal.Focal(
self.client,
callbacks=[
focal.Callback(
name="rewrite_text",
arguments=[
focal.Argument(name="text", type="str", preserve_newlines=True),
],
fn=rewrite_text,
multiple=False,
),
],
max_calls=1,
retries=1,
scene=self.scene,
analysis=analysis,
text=text,
)
revision = extracted["revision"]
if revision is None:
log.debug(
"revision_rewrite: no <REVISION> found in response, keeping original"
)
return original_text
if loading_status:
loading_status("Editor - Rewriting text...")
await focal_handler.request(
"editor.revision-rewrite",
)
try:
revision = focal_handler.state.calls[0].result
except Exception as e:
log.error("revision_rewrite: error", error=e)
# Guard: if the rewrite is substantially longer than the original,
# the model likely expanded beyond the "do not make it longer"
# instruction — discard it.
if len(revision) > len(text) * REWRITE_MAX_LENGTH_RATIO:
log.warning(
"revision_rewrite: revision is too long, discarding",
original_len=len(text),
revision_len=len(revision),
ratio=_format_length_ratio(len(revision), len(text)),
)
return original_text
emission.response = revision
@@ -1105,7 +1120,7 @@ class RevisionMixin:
async def revision_unslop(
self,
info: RevisionInformation,
response_length: int = 768,
response_length: int = REVISION_RESPONSE_HEADROOM,
) -> str:
"""
Unslop the text
@@ -1183,7 +1198,7 @@ class RevisionMixin:
"revision_unslop: fix is too long, discarding",
original_len=len(text),
fix_len=len(fix),
ratio=f"{len(fix) / len(text):.2f}",
ratio=_format_length_ratio(len(fix), len(text)),
)
return original_text

View File

@@ -1,99 +0,0 @@
{#-
Extractors:
- response: AsIsExtractor()
Override example:
{{ set_anchor_extractor("response", "<MY_TAG>", "</MY_TAG>") }}
{{ set_as_is_extractor("response") }}
-#}
{# Using common templates approach #}
{% set volatile_placement = volatile_context_placement() %}
{# RENDERED CONTEXT #}
{% set rendered_context -%}
{% include "character-context.jinja2" -%}
<|SECTION:ADDITIONAL INFORMATION|>
{% include "extra-context-static.jinja2" -%}
<|CLOSE_SECTION|>
{% include "scene-intent.jinja2" %}
{% endset %}
{# END RENDERED CONTEXT #}
{# VOLATILE CONTEXT #}
{% set volatile_context_text %}
{% include "memory-context.jinja2" %}
{% include "extra-context-dynamic.jinja2" %}
{% endset %}
{# END VOLATILE CONTEXT #}
{# SCENE CONTEXT #}
{% set scene_context_text %}
{% set scene_context = scene.context_history(
budget=max_tokens-300-count_tokens(rendered_context)-count_tokens(volatile_context_text),
min_dialogue=15,
sections=False,
keep_director=False,
chapter_labels=True
)
-%}
{% for scene_line in scene_context -%}
{{ scene_line }}
{% endfor %}
{% endset %}
{# END SCENE CONTEXT #}
{# RENDER PROMPT #}
{{ rendered_context }}
{% if volatile_placement != "after_history" %}
{% with _text=scene_context_text %}{% include "internal-note-help.jinja2" %}{% endwith %}
{{ volatile_context_text }}
{% endif %}
<|SECTION:SCENE|>
{{ scene_context_text }}
<|CLOSE_SECTION|>
{% if volatile_placement == "after_history" %}
{% with _text=scene_context_text %}{% include "internal-note-help.jinja2" %}{% endwith %}
{{ volatile_context_text }}
{% endif %}
<|SECTION:DRAFT TEXT|>
{{ text }}
<|CLOSE_SECTION|>
{% set issues = 0 %}
{# repetition #}{% if repetition %}
{% set issues = issues + 1 %}
<|SECTION:ISSUE {{ issues }}: REPEATED TEXT|>
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
{% for repeat in repetition %}- MATCH: `{{ repeat["text_a"].strip() }}`
{% endfor %}
<|CLOSE_SECTION|>
{# end repetition #}{% endif %}
{# unwanted prose #}{% if bad_prose %}
{% set issues = issues + 1 %}
<|SECTION:ISSUE {{ issues }}: UNWANTED PROSE|>
These phrases or words have been identified by the director as bad, and MUST be changed accordingly.
Important: These are semantic matches based on meaning, not literal text. Focus on the underlying concept or idea that triggered the match, rather than expecting exact phrase matches.
You must state your greater understanding of why these issues were flagged in the first place so you do not replace them with something equally bad or worse.
{% for phrase in bad_prose %}- MATCH: `{{ phrase.phrase }}`
- INSTRUCTIONS: {{ phrase.instructions }}
{% endfor %}
<|CLOSE_SECTION|>
{# end unwanted prose #}{% endif %}
{% include "dynamic-instructions.jinja2" %}
<|SECTION:TASK|>
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially.
<|CLOSE_SECTION|>

View File

@@ -1,26 +1,105 @@
<|SECTION:DRAFT TEXT|>
{{ text }}
<|CLOSE_SECTION|>
<|SECTION:FUNCTION CALLING INSTRUCTIONS|>
{{ focal.render_instructions() }}
{#-
Extractors:
- revision: AnchorExtractor("<REVISION>", "</REVISION>")
{{
focal.callbacks.rewrite_text.render(
"Rewrite the draft text to fix it according to the provided analysis. Double quotes \" should be maintained (escape if you have to)",
text="The revision of the ENTIRE draft text.",
examples=[
{ "text": "... rewrite the text here ..." },
],
)
}}
Override example:
{{ set_anchor_extractor("revision", "<MY_TAG>", "</MY_TAG>") }}
-#}
{# Using common templates approach #}
{% set volatile_placement = volatile_context_placement() %}
{# RENDERED CONTEXT #}
{% set rendered_context -%}
{% include "character-context.jinja2" -%}
<|SECTION:ADDITIONAL INFORMATION|>
{% include "extra-context-static.jinja2" -%}
<|CLOSE_SECTION|>
{% include "scene-intent.jinja2" %}
{% endset %}
{# END RENDERED CONTEXT #}
{# VOLATILE CONTEXT #}
{% set volatile_context_text %}
{% include "memory-context.jinja2" %}
{% include "extra-context-dynamic.jinja2" %}
{% endset %}
{# END VOLATILE CONTEXT #}
{# SCENE CONTEXT #}
{% set scene_context_text %}
{% set scene_context = scene.context_history(
budget=max_tokens-300-count_tokens(rendered_context)-count_tokens(volatile_context_text),
min_dialogue=15,
sections=False,
keep_director=False,
chapter_labels=True
)
-%}
{% for scene_line in scene_context -%}
{{ scene_line }}
{% endfor %}
{% endset %}
{# END SCENE CONTEXT #}
{# RENDER PROMPT #}
{{ rendered_context }}
{% if volatile_placement != "after_history" %}
{% with _text=scene_context_text %}{% include "internal-note-help.jinja2" %}{% endwith %}
{{ volatile_context_text }}
{% endif %}
<|SECTION:SCENE|>
{{ scene_context_text }}
<|CLOSE_SECTION|>
{% if volatile_placement == "after_history" %}
{% with _text=scene_context_text %}{% include "internal-note-help.jinja2" %}{% endwith %}
{{ volatile_context_text }}
{% endif %}
<|SECTION:DRAFT TEXT|>
{{ text }}
<|CLOSE_SECTION|>
<|SECTION:ANALYSIS|>
{{ analysis }}
{% set issues = 0 %}
{# repetition #}{% if repetition %}
{% set issues = issues + 1 %}
<|SECTION:ISSUE {{ issues }}: REPEATED TEXT|>
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
{% for repeat in repetition %}- MATCH: `{{ repeat["text_a"].strip() }}`
{% endfor %}
<|CLOSE_SECTION|>
{# end repetition #}{% endif %}
{# unwanted prose #}{% if bad_prose %}
{% set issues = issues + 1 %}
<|SECTION:ISSUE {{ issues }}: UNWANTED PROSE|>
These phrases or words have been identified by the director as bad, and MUST be changed accordingly.
Important: These are semantic matches based on meaning, not literal text. Focus on the underlying concept or idea that triggered the match, rather than expecting exact phrase matches.
You must state your greater understanding of why these issues were flagged in the first place so you do not replace them with something equally bad or worse.
{% for phrase in bad_prose %}- MATCH: `{{ phrase.phrase }}`
- INSTRUCTIONS: {{ phrase.instructions }}
{% endfor %}
<|CLOSE_SECTION|>
{# end unwanted prose #}{% endif %}
{% include "dynamic-instructions.jinja2" %}
<|SECTION:TASK|>
Call the `rewrite_text` function with the exact new revision of the draft text.
<|CLOSE_SECTION|>
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
{% include "response-length.jinja2" %}
<|CLOSE_SECTION|>

View File

@@ -0,0 +1,63 @@
## Characters
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
## Additional information
## Classification
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
## Intention of this story
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
## Intention of the current scene
An exploration scene
The hero explores the forest.
## Scene
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
## Draft text
The forest was dark and mysterious.
## Issue 1: repeated text
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
## Task
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your response must fit within 6 paragraphs.

View File

@@ -0,0 +1,63 @@
## Characters
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
## Additional information
## Classification
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
## Intention of this story
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
## Intention of the current scene
An exploration scene
The hero explores the forest.
## Scene
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
## Draft text
The forest was dark and mysterious.
## Issue 1: repeated text
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
## Task
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your response must fit within 6 paragraphs.

View File

@@ -0,0 +1,63 @@
## Characters
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
## Additional information
## Classification
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
## Intention of this story
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
## Intention of the current scene
An exploration scene
The hero explores the forest.
## Scene
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
## Draft text
The forest was dark and mysterious.
## Issue 1: repeated text
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
## Task
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your response must fit within 6 paragraphs.

View File

@@ -0,0 +1,63 @@
## Characters
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
## Additional information
## Classification
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
## Intention of this story
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
## Intention of the current scene
An exploration scene
The hero explores the forest.
## Scene
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
## Draft text
The forest was dark and mysterious.
## Issue 1: repeated text
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
## Task
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your response must fit within 6 paragraphs.

View File

@@ -0,0 +1,63 @@
## Characters
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
## Additional information
## Classification
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
## Intention of this story
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
## Intention of the current scene
An exploration scene
The hero explores the forest.
## Scene
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
## Draft text
The forest was dark and mysterious.
## Issue 1: repeated text
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
## Task
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your final answer must fit within 6 paragraphs.

View File

@@ -0,0 +1,67 @@
<CHARACTERS>
### Hero
name: Hero
A test character.
### Elena
name: Elena
A test character.
</CHARACTERS>
<CLASSIFICATION>
Content Classification: Fantasy adventure story
Narrative Perspective: <Mock name='mock.perspective' id='NORMALIZED'>
</CLASSIFICATION>
<INTENTION_OF_THIS_STORY>
The overarching intention of this story. Use it to guide your decisions in the scene.
A fantasy adventure story.
</INTENTION_OF_THIS_STORY>
<INTENTION_OF_THE_CURRENT_SCENE>
An exploration scene
The hero explores the forest.
</INTENTION_OF_THE_CURRENT_SCENE>
<SCENE>
Elena: Hello there, traveler.
The sun filters through the leaves above.
Marcus: What brings you to these woods?
</SCENE>
<DRAFT_TEXT>
The forest was dark and mysterious.
</DRAFT_TEXT>
<ISSUE_1:_REPEATED_TEXT>
These sentences have been classified as repetition and must either be substantially rewritten or removed. When removing a sentence, substitute something else, that's meaningful to the story and context.
- MATCH: `The forest was dark and mysterious.`
</ISSUE_1:_REPEATED_TEXT>
<TASK>
1. Analyze each issue separately, taking into account the instructions and guidelines for it.
2. Specifically compare against previous messages, instead of rewriting the same information often it is better to just drop the repetitive sentence entirely. Its no use to convey the SAME information again just with different words. Repeat back your understanding of this concept.
3. Tell us your confident plan for fixing the issues.
4. Rewrite the ENTIRE draft text to fix the issues, the best you can, according to the plan you made.
- Keep your changes minimal, don't get carried away.
- YOU MUST NOT MAKE IT LONGER.
- YOU MUST NOT REWRITE PARTS THAT WEREN'T EXPLICITLY FLAGGED.
- Make one revision in total - so analyze issues first, then fix them all at once!
- Wrap ONLY the final rewritten draft in `<REVISION>` tags, exactly once, like this:
<REVISION>
... the full rewritten draft text goes here ...
</REVISION>
- The text inside `<REVISION>` must be the complete replacement for the draft — no commentary, no partial excerpts.
Repeat back your understanding of the task, then work and report each step 1 - 4, sequentially. End your response with the `<REVISION>` block.
The length of your response must fit within 6 paragraphs.
</TASK>

View File

@@ -6,7 +6,7 @@ against stored baseline files. Run with --update-baselines to create/update.
"""
import pytest
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, Mock
from ..conftest import mock_llm_client # noqa: F401
from ..test_editor_templates import ( # noqa: F401
@@ -35,6 +35,48 @@ class TestEditorBaselines:
await editor.add_detail(content="Elena: Hello there.", character=character)
baseline_checker(capture_prompt(editor), AGENT, "add_detail")
@pytest.mark.asyncio
async def test_revision_rewrite(
self,
active_context,
mock_scene,
mock_memory_agent,
baseline_checker,
):
editor = active_context
editor.actions["revision"].enabled = True
editor.actions["revision"].config["revision_method"].value = "rewrite"
editor.actions["revision"].config["min_issues"].value = 1
# Provide history message that the draft text repeats
history_messages = [
Mock(message="The forest was dark and mysterious.", typ="narrator"),
]
mock_scene.collect_messages = Mock(return_value=history_messages)
# Force a repetition match so the rewrite path actually runs
mock_memory_agent.compare_string_lists = AsyncMock(
return_value={
"similarity_matches": [[0, 0, 0.9]],
"cosine_similarity_matrix": [],
}
)
# Single-prompt flow: return a well-formed <REVISION> response
editor.client.send_prompt = AsyncMock(
return_value="<REVISION>The forest was silent.</REVISION>"
)
from talemate.agents.editor.revision import RevisionInformation
info = RevisionInformation(
text="The forest was dark and mysterious.",
character=None,
)
await editor.revision_rewrite(info)
baseline_checker(capture_prompt(editor), AGENT, "revision_rewrite")
@pytest.mark.asyncio
async def test_revision_unslop(
self,

View File

@@ -7,7 +7,7 @@ to prompt rendering to LLM call, without making actual API calls.
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch
from unittest.mock import Mock, AsyncMock
import talemate.instance as instance
from talemate.agents.editor import EditorAgent
@@ -245,26 +245,18 @@ class TestEditorAddDetailMethod:
class TestEditorRevisionRewriteMethod:
"""Tests for the revision_rewrite method that calls revision templates."""
@pytest.mark.asyncio
async def test_revision_rewrite_calls_client_when_issues_found(
self, active_context, mock_scene, mock_memory_agent
):
"""Test that revision_rewrite calls the LLM client when issues are found."""
editor = active_context
# Enable revision
@staticmethod
def _enable_rewrite_with_repetition(editor, mock_scene, mock_memory_agent):
"""Enable rewrite + force one repetition match so the rewrite path runs."""
editor.actions["revision"].enabled = True
editor.actions["revision"].config["revision_method"].value = "rewrite"
editor.actions["revision"].config["min_issues"].value = 1
# Provide history messages for comparison
history_messages = [
Mock(message="The forest was dark and mysterious.", typ="narrator"),
]
mock_scene.collect_messages = Mock(return_value=history_messages)
# Mock memory agent to return similarity matches (repetition detected)
# [text_index, history_index, similarity]
mock_memory_agent.compare_string_lists = AsyncMock(
return_value={
"similarity_matches": [[0, 0, 0.9]],
@@ -272,32 +264,105 @@ class TestEditorRevisionRewriteMethod:
}
)
# Mock focal handler response
with patch("talemate.game.focal.Focal") as mock_focal_class:
mock_focal = AsyncMock()
mock_focal.state = Mock()
mock_focal.state.calls = [Mock(result="Revised text here.")]
mock_focal.request = AsyncMock()
mock_focal_class.return_value = mock_focal
@pytest.mark.asyncio
async def test_revision_rewrite_extracts_revision_tag(
self, active_context, mock_scene, mock_memory_agent
):
"""Rewrite should run a single prompt and return the <REVISION> content."""
editor = active_context
self._enable_rewrite_with_repetition(editor, mock_scene, mock_memory_agent)
from talemate.agents.editor.revision import RevisionInformation
info = RevisionInformation(
text="The forest was dark. The forest was quiet.",
character=None,
expected_revision = "The woods were silent under heavy cloud."
editor.client.send_prompt = AsyncMock(
return_value=(
"Analysis: the draft repeats a prior line.\n"
f"<REVISION>{expected_revision}</REVISION>"
)
)
await editor.revision_rewrite(info)
from talemate.agents.editor.revision import RevisionInformation
# Verify the client's send_prompt was called for analysis
editor.client.send_prompt.assert_called()
info = RevisionInformation(
text="The forest was dark and mysterious.",
character=None,
)
# Get the prompt that was sent
call_args = editor.client.send_prompt.call_args
prompt_text = str(call_args[0][0])
response = await editor.revision_rewrite(info)
# Verify the prompt contains the text
assert "forest" in prompt_text.lower()
# Only the analysis prompt should fire — no secondary rewrite prompt
assert editor.client.send_prompt.call_count == 1
assert response == expected_revision
assert "<REVISION>" not in response
@pytest.mark.asyncio
async def test_revision_rewrite_returns_original_when_no_revision_tag(
self, active_context, mock_scene, mock_memory_agent
):
"""If the analysis response has no <REVISION> tag, fall back to original."""
editor = active_context
self._enable_rewrite_with_repetition(editor, mock_scene, mock_memory_agent)
editor.client.send_prompt = AsyncMock(
return_value="Analysis only, no revision produced."
)
from talemate.agents.editor.revision import RevisionInformation
original_text = "The forest was dark and mysterious."
info = RevisionInformation(text=original_text, character=None)
response = await editor.revision_rewrite(info)
assert response == original_text
assert editor.client.send_prompt.call_count == 1
@pytest.mark.asyncio
async def test_revision_rewrite_discards_oversized_revision(
self, active_context, mock_scene, mock_memory_agent
):
"""Rewrites longer than REWRITE_MAX_LENGTH_RATIO should be discarded."""
editor = active_context
self._enable_rewrite_with_repetition(editor, mock_scene, mock_memory_agent)
original_text = "The forest was dark and mysterious."
# Produce a fix ~3x longer than original — well above the guardrail
bloated = " ".join([original_text] * 3)
editor.client.send_prompt = AsyncMock(
return_value=f"<REVISION>{bloated}</REVISION>"
)
from talemate.agents.editor.revision import RevisionInformation
info = RevisionInformation(text=original_text, character=None)
response = await editor.revision_rewrite(info)
assert response == original_text
assert editor.client.send_prompt.call_count == 1
@pytest.mark.asyncio
async def test_revision_rewrite_reattaches_character_prefix(
self, active_context, mock_scene, mock_memory_agent
):
"""Character dialogue should have the `Name: ` prefix preserved."""
editor = active_context
character = mock_scene.get_character("Elena")
self._enable_rewrite_with_repetition(editor, mock_scene, mock_memory_agent)
editor.client.send_prompt = AsyncMock(
return_value='<REVISION>"A new line entirely."</REVISION>'
)
from talemate.agents.editor.revision import RevisionInformation
info = RevisionInformation(
text='Elena: "The forest was dark and mysterious."',
character=character,
)
response = await editor.revision_rewrite(info)
assert response == 'Elena: "A new line entirely."'
@pytest.mark.asyncio
async def test_revision_rewrite_returns_original_when_no_issues(