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.
|
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.
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|