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. 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 ##### Revision Method
Which method to use to fix issues. 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.memory.rag import MemoryRAGMixin
from talemate.agents.editor.revision import RevisionMixin from talemate.agents.editor.revision import RevisionMixin
from talemate.agents.editor.websocket_handler import EditorWebsocketHandler
if TYPE_CHECKING: if TYPE_CHECKING:
from talemate.agents.conversation import ConversationAgentEmission from talemate.agents.conversation import ConversationAgentEmission
@ -38,6 +39,7 @@ class EditorAgent(
agent_type = "editor" agent_type = "editor"
verbose_name = "Editor" verbose_name = "Editor"
websocket_handler = EditorWebsocketHandler
@classmethod @classmethod
def init_actions(cls) -> dict[str, AgentAction]: def init_actions(cls) -> dict[str, AgentAction]:

View file

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

View file

@ -8,7 +8,7 @@
<|SECTION:ISSUE {{ issues }}: REPEATED TEXT|> <|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. 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 %} {% endfor %}
<|CLOSE_SECTION|> <|CLOSE_SECTION|>
{# end repetition #}{% endif %} {# end repetition #}{% endif %}
@ -16,8 +16,11 @@ These sentences have been classified as repetition and must either be substantia
{% set issues = issues + 1 %} {% set issues = issues + 1 %}
<|SECTION:ISSUE {{ issues }}: UNWANTED PROSE|> <|SECTION:ISSUE {{ issues }}: UNWANTED PROSE|>
These phrases or words have been identified by the director as bad, and MUST be changed accordingly. 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 %} {% endfor %}
<|CLOSE_SECTION|> <|CLOSE_SECTION|>
{# end unwanted prose #}{% endif %} {# end unwanted prose #}{% endif %}

View file

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

View file

@ -59,6 +59,12 @@
Create Pin Create Pin
</v-chip> </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 --> <!-- 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-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> <v-icon class="mr-1">mdi-source-fork</v-icon>
@ -76,8 +82,48 @@
import { parseText } from '@/utils/textParser'; import { parseText } from '@/utils/textParser';
export default { export default {
props: ['character', 'text', 'color', 'message_id', 'uxLocked', 'isLastMessage'], //props: ['character', 'text', 'color', 'message_id', 'uxLocked', 'isLastMessage'],
inject: ['requestDeleteMessage', 'getWebsocket', 'createPin', 'forkSceneInitiate', 'fixMessageContinuityErrors', 'autocompleteRequest', 'autocompleteInfoMessage', 'getMessageStyle'], 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: { computed: {
parts() { parts() {
return parseText(this.text); return parseText(this.text);

View file

@ -44,6 +44,12 @@
Create Pin Create Pin
</v-chip> </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 --> <!-- fork scene -->
<v-chip size="x-small" class="ml-2" label color="primary" v-if="!editing && hovered" variant="outlined" <v-chip size="x-small" class="ml-2" label color="primary" v-if="!editing && hovered" variant="outlined"
@click="forkSceneInitiate(message_id)" :disabled="uxLocked"> @click="forkSceneInitiate(message_id)" :disabled="uxLocked">
@ -65,8 +71,41 @@
import { parseText } from '@/utils/textParser'; import { parseText } from '@/utils/textParser';
export default { export default {
props: ['text', 'message_id', 'uxLocked', 'isLastMessage'], // props: ['text', 'message_id', 'uxLocked', 'isLastMessage'],
inject: ['requestDeleteMessage', 'getWebsocket', 'createPin', 'forkSceneInitiate', 'fixMessageContinuityErrors', 'autocompleteRequest', 'autocompleteInfoMessage', 'getMessageStyle', 'openWorldStateManager'],
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: { computed: {
parts() { parts() {
return parseText(this.text); return parseText(this.text);

View file

@ -10,7 +10,7 @@
<div v-if="message.type === 'character' || message.type === 'processing_input'" <div v-if="message.type === 'character' || message.type === 'processing_input'"
:class="`message ${message.type}`" :id="`message-${message.id}`" :style="{ borderColor: message.color }"> :class="`message ${message.type}`" :id="`message-${message.id}`" :style="{ borderColor: message.color }">
<div class="character-message"> <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> </div>
<div v-else-if="message.type === 'request_input' && message.choices"> <div v-else-if="message.type === 'request_input' && message.choices">
@ -43,7 +43,7 @@
</div> </div>
<div v-else-if="message.type === 'narrator'" :class="`message ${message.type}`"> <div v-else-if="message.type === 'narrator'" :class="`message ${message.type}`">
<div class="narrator-message" :id="`message-${message.id}`"> <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> </div>
<div v-else-if="message.type === 'director' && !getMessageTypeHidden(message.type)" :class="`message ${message.type}`"> <div v-else-if="message.type === 'director' && !getMessageTypeHidden(message.type)" :class="`message ${message.type}`">
@ -99,6 +99,9 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
agentStatus: {
type: Object,
}
}, },
components: { components: {
CharacterMessage, 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'], inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput'],
provide() { provide() {
return { return {
@ -131,6 +139,7 @@ export default {
forkSceneInitiate: this.forkSceneInitiate, forkSceneInitiate: this.forkSceneInitiate,
getMessageColor: this.getMessageColor, getMessageColor: this.getMessageColor,
getMessageStyle: this.getMessageStyle, getMessageStyle: this.getMessageStyle,
reviseMessage: this.reviseMessage,
} }
}, },
methods: { 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) { handleMessage(data) {
var i; var i;

View file

@ -186,7 +186,12 @@
</v-alert> </v-alert>
</div> </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"> <div ref="sceneToolsContainer">
<SceneTools <SceneTools
@ -801,6 +806,7 @@ export default {
// active - has the agent been active in the last 5 seconds? // active - has the agent been active in the last 5 seconds?
recentlyActive: recentlyActive, recentlyActive: recentlyActive,
details: data.client, details: data.client,
actions: data.data.actions,
} }
if(recentlyActive && !busy) { if(recentlyActive && !busy) {