editor revision: automatic revision setting, prompt tweaks, docs

This commit is contained in:
vegu-ai-tools 2025-05-11 21:55:28 +03:00
parent 83ba598790
commit 3a1ac54b90
15 changed files with 165 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -49,6 +49,10 @@ This means it comes at a noticable delay IF it finds issues, but the improvement
Check this to enable revision.
##### Automatic Revision
Check this to enable automatic revision - this will analyze each incoming actor or narrator message and attempt to fix it if there are issues.
##### Revision Method
Which method to use to fix issues.

View file

@ -15,6 +15,7 @@ import talemate.agents.editor.nodes
from talemate.agents.memory.rag import MemoryRAGMixin
from talemate.agents.editor.revision import RevisionMixin
from talemate.agents.editor.websocket_handler import EditorWebsocketHandler
if TYPE_CHECKING:
from talemate.agents.conversation import ConversationAgentEmission
@ -38,6 +39,7 @@ class EditorAgent(
agent_type = "editor"
verbose_name = "Editor"
websocket_handler = EditorWebsocketHandler
@classmethod
def init_actions(cls) -> dict[str, AgentAction]:

View file

@ -1,6 +1,7 @@
from typing import TYPE_CHECKING
import structlog
import uuid
import pydantic
import re
from talemate.agents.base import (
set_processing,
@ -52,16 +53,30 @@ detect_bad_prose_condition = AgentActionConditional(
value=True,
)
class RevisionContextState(pydantic.BaseModel):
message_id: int | None = None
revision_disabled_context = ContextVar("revision_disabled", default=False)
revision_context = ContextVar("revision_context", default=RevisionContextState())
class RevisionDisabled:
def __enter__(self):
self.token = revision_disabled_context.set(True)
def __exit__(self, exc_type, exc_value, traceback):
revision_disabled_context.reset(self.token)
class RevisionContext:
def __init__(self, message_id: int | None = None):
self.message_id = message_id
def __enter__(self):
self.token = revision_context.set(RevisionContextState(message_id=self.message_id))
def __exit__(self, exc_type, exc_value, traceback):
revision_context.reset(self.token)
class RevisionMixin:
"""
@ -79,6 +94,13 @@ class RevisionMixin:
icon="mdi-typewriter",
description="Remove / rewrite content based on criteria and instructions.",
config={
"automatic_revision": AgentActionConfig(
type="bool",
label="Automatic revision",
description="Enable / Disable automatic revision.",
value=True,
quick_toggle=True,
),
"revision_method": AgentActionConfig(
type="text",
label="Revision method",
@ -180,6 +202,10 @@ class RevisionMixin:
def revision_enabled(self):
return self.actions["revision"].enabled
@property
def revision_automatic_enabled(self):
return self.actions["revision"].config["automatic_revision"].value
@property
def revision_method(self):
return self.actions["revision"].config["revision_method"].value
@ -234,7 +260,7 @@ class RevisionMixin:
Called when a conversation or narrator message is generated
"""
if not self.revision_enabled:
if not self.revision_enabled or not self.revision_automatic_enabled:
return
try:
@ -262,9 +288,12 @@ class RevisionMixin:
scene:"Scene" = self.scene
ctx = revision_context.get()
messages = scene.collect_messages(
typ=["narrator", "character"],
max_messages=self.revision_repetition_range
max_messages=self.revision_repetition_range,
start_idx=scene.message_index(ctx.message_id) -1 if ctx.message_id else None
)
return_messages = []
@ -600,7 +629,7 @@ class RevisionMixin:
if detect_bad_prose:
bad_prose_identified = await self.revision_detect_bad_prose(text)
for identified in bad_prose_identified:
issues.append(f"Bad prose: `{identified['phrase']}` (reason: {identified['reason']}, instructions: {identified['instructions']})")
issues.append(f"Bad prose: `{identified['phrase']}` (reason: {identified['reason']}, matched: {identified['matched']}, instructions: {identified['instructions']})")
log.debug("revision_rewrite: bad_prose_identified", bad_prose_identified=bad_prose_identified)
# Step 3 - Check if we have enough issues to warrant a rewrite

View file

@ -8,7 +8,7 @@
<|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 %}- `{{ repeat["text_a"].strip() }}`
{% for repeat in repetition %}- MATCH: `{{ repeat["text_a"].strip() }}`
{% endfor %}
<|CLOSE_SECTION|>
{# end repetition #}{% endif %}
@ -16,8 +16,11 @@ These sentences have been classified as repetition and must either be substantia
{% 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.
{% for phrase in bad_prose %}- `{{ phrase.phrase }}` -- {{ phrase.instructions }}
{% for phrase in bad_prose %}- MATCH: `{{ phrase.phrase }}`
- TRIGGER: "{{ phrase.matched }}"
- INSTRUCTIONS: {{ phrase.instructions }}
{% endfor %}
<|CLOSE_SECTION|>
{# end unwanted prose #}{% endif %}

View file

@ -1124,6 +1124,7 @@ class Scene(Emitter):
max_iterations: int = 100,
max_messages: int | None = None,
stop_on_time_passage: bool = False,
start_idx: int | None = None,
):
"""
Finds all messages in the history that match the given typ and source
@ -1135,7 +1136,11 @@ class Scene(Emitter):
messages = []
iterations = 0
collected = 0
for idx in range(len(self.history) - 1, -1, -1):
if start_idx is None:
start_idx = len(self.history) - 1
for idx in range(start_idx, -1, -1):
message = self.history[idx]
if (not typ or message.typ in typ) and (
not source or message.source == source

View file

@ -59,6 +59,12 @@
Create Pin
</v-chip>
<!-- revision -->
<v-chip size="x-small" class="ml-2" label color="dirty" v-if="!editing && hovered && editorRevisionsEnabled && isLastMessage" variant="outlined" @click="reviseMessage(message_id)" :disabled="uxLocked">
<v-icon class="mr-1">mdi-typewriter</v-icon>
Editor Revision
</v-chip>
<!-- fork scene -->
<v-chip size="x-small" class="ml-2" label color="primary" v-if="!editing && hovered" variant="outlined" @click="forkSceneInitiate(message_id)" :disabled="uxLocked">
<v-icon class="mr-1">mdi-source-fork</v-icon>
@ -76,8 +82,48 @@
import { parseText } from '@/utils/textParser';
export default {
props: ['character', 'text', 'color', 'message_id', 'uxLocked', 'isLastMessage'],
inject: ['requestDeleteMessage', 'getWebsocket', 'createPin', 'forkSceneInitiate', 'fixMessageContinuityErrors', 'autocompleteRequest', 'autocompleteInfoMessage', 'getMessageStyle'],
//props: ['character', 'text', 'color', 'message_id', 'uxLocked', 'isLastMessage'],
props: {
character: {
type: String,
required: true,
},
text: {
type: String,
required: true,
},
color: {
type: String,
required: true,
},
message_id: {
type: Number,
required: true,
},
uxLocked: {
type: Boolean,
required: true,
},
isLastMessage: {
type: Boolean,
required: true,
},
editorRevisionsEnabled: {
type: Boolean,
default: false,
},
},
inject: [
'requestDeleteMessage',
'getWebsocket',
'createPin',
'forkSceneInitiate',
'fixMessageContinuityErrors',
'autocompleteRequest',
'autocompleteInfoMessage',
'getMessageStyle',
'reviseMessage',
],
computed: {
parts() {
return parseText(this.text);

View file

@ -44,6 +44,12 @@
Create Pin
</v-chip>
<!-- revision -->
<v-chip size="x-small" class="ml-2" label color="dirty" v-if="!editing && hovered && editorRevisionsEnabled && isLastMessage" variant="outlined" @click="reviseMessage(message_id)" :disabled="uxLocked">
<v-icon class="mr-1">mdi-typewriter</v-icon>
Editor Revision
</v-chip>
<!-- fork scene -->
<v-chip size="x-small" class="ml-2" label color="primary" v-if="!editing && hovered" variant="outlined"
@click="forkSceneInitiate(message_id)" :disabled="uxLocked">
@ -65,8 +71,41 @@
import { parseText } from '@/utils/textParser';
export default {
props: ['text', 'message_id', 'uxLocked', 'isLastMessage'],
inject: ['requestDeleteMessage', 'getWebsocket', 'createPin', 'forkSceneInitiate', 'fixMessageContinuityErrors', 'autocompleteRequest', 'autocompleteInfoMessage', 'getMessageStyle', 'openWorldStateManager'],
// props: ['text', 'message_id', 'uxLocked', 'isLastMessage'],
props: {
text: {
type: String,
required: true
},
message_id: {
required: true
},
uxLocked: {
type: Boolean,
required: true
},
isLastMessage: {
type: Boolean,
required: true
},
editorRevisionsEnabled: {
type: Boolean,
default: false,
},
},
inject: [
'requestDeleteMessage',
'getWebsocket',
'createPin',
'forkSceneInitiate',
'fixMessageContinuityErrors',
'autocompleteRequest',
'autocompleteInfoMessage',
'getMessageStyle',
'openWorldStateManager',
'reviseMessage',
],
computed: {
parts() {
return parseText(this.text);

View file

@ -10,7 +10,7 @@
<div v-if="message.type === 'character' || message.type === 'processing_input'"
:class="`message ${message.type}`" :id="`message-${message.id}`" :style="{ borderColor: message.color }">
<div class="character-message">
<CharacterMessage :character="message.character" :text="message.text" :color="message.color" :message_id="message.id" :uxLocked="uxLocked" :isLastMessage="index === messages.length - 1" />
<CharacterMessage :character="message.character" :text="message.text" :color="message.color" :message_id="message.id" :uxLocked="uxLocked" :isLastMessage="index === messages.length - 1" :editorRevisionsEnabled="editorRevisionsEnabled" />
</div>
</div>
<div v-else-if="message.type === 'request_input' && message.choices">
@ -43,7 +43,7 @@
</div>
<div v-else-if="message.type === 'narrator'" :class="`message ${message.type}`">
<div class="narrator-message" :id="`message-${message.id}`">
<NarratorMessage :text="message.text" :message_id="message.id" :uxLocked="uxLocked" :isLastMessage="index === messages.length - 1" />
<NarratorMessage :text="message.text" :message_id="message.id" :uxLocked="uxLocked" :isLastMessage="index === messages.length - 1" :editorRevisionsEnabled="editorRevisionsEnabled" />
</div>
</div>
<div v-else-if="message.type === 'director' && !getMessageTypeHidden(message.type)" :class="`message ${message.type}`">
@ -99,6 +99,9 @@ export default {
type: Boolean,
default: false,
},
agentStatus: {
type: Object,
}
},
components: {
CharacterMessage,
@ -122,6 +125,11 @@ export default {
},
}
},
computed: {
editorRevisionsEnabled() {
return this.agentStatus && this.agentStatus.editor && this.agentStatus.editor.actions && this.agentStatus.editor.actions["revision"] && this.agentStatus.editor.actions["revision"].enabled;
}
},
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput'],
provide() {
return {
@ -131,6 +139,7 @@ export default {
forkSceneInitiate: this.forkSceneInitiate,
getMessageColor: this.getMessageColor,
getMessageStyle: this.getMessageStyle,
reviseMessage: this.reviseMessage,
}
},
methods: {
@ -267,6 +276,14 @@ export default {
}));
},
reviseMessage(message_id) {
this.getWebsocket().send(JSON.stringify({
type: 'editor',
action: 'request_revision',
message_id: message_id,
}));
},
handleMessage(data) {
var i;

View file

@ -186,7 +186,12 @@
</v-alert>
</div>
<SceneMessages ref="sceneMessages" :appearance-config="appConfig ? appConfig.appearance : {}" :ux-locked="uxLocked" />
<SceneMessages
ref="sceneMessages"
:appearance-config="appConfig ? appConfig.appearance : {}"
:ux-locked="uxLocked"
:agent-status="agentStatus"
/>
<div ref="sceneToolsContainer">
<SceneTools
@ -801,6 +806,7 @@ export default {
// active - has the agent been active in the last 5 seconds?
recentlyActive: recentlyActive,
details: data.client,
actions: data.data.actions,
}
if(recentlyActive && !busy) {