editor revision: automatic revision setting, prompt tweaks, docs
BIN
docs/img/0.30.0/editor-has-messages.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
docs/img/0.30.0/editor-revision-history.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
docs/img/0.30.0/editor-revision-issue-identified.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/img/0.30.0/editor-revision-rewriting.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 103 KiB |
|
@ -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.
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|