mirror of
https://github.com/vegu-ai/talemate.git
synced 2025-12-15 19:27:47 +01:00
Prep 0.11.0 (#19)
* dolphin mistral template * removate trailing \n before attaching the model response * improve prompt and validator for generated human age * fix issue where errors during character creation process would not be communicated to the ux and the character creator would appear stuck * add dolphin mistral to list * add talemate_env * poetry relock * add json schema for talemate scene files * fix issues with pydantic after version upgrade * add json extrac util functions * fix pydantic model * use extract json function * scene generator, better scene name prompt * OpenHermes-2-Mistral * alpaca base template Amethyst 20B template * character description is no longer part of the sheet and needs to be added separately * fix pydantic validation * fix issue where sometimes partial emote strings were kept at the end of dialogue * no need to commit character name to memory * dedupe prompts * clean up extra linebreaks in prompts * experimental editor agent agent signals first progress * take out hardcoded example * amethyst llm prompt template * editor agent disableable agent edit modal tweaks * world state agent agent action config schema * director agent disableable remove automatic actions config from ux (deprecated) * fix responsive update when toggling enable on or off in agent dialog * prompt adjustments fix divine intellect preset (mirostat values were way off) fix world state regenerating every turn regardless of setting * move templates for world state from summarizer to worldstate agent * conversation agent generation lenght setting * conversation agent jiggle attribute (randomize offset to certain inference parameters) * relabel * scene cover image set to cover as much space as it can * add character sheet to dialogue example generate prompt * character creator agent mixin use set_processing * add <|im_end|> to stopping strings * add random number gen to template functions * SynthIA and Tiefighter * create new persisted characters ouf of world state natural flow option for conversation agent to help guide multi character conversations * conversation agent natural flow improvements * fix bug with 1h time passage option * some templates * poetry relock * fix config validation * fix issues when detemrining scene history context length to stay within budget * fixes to world state json parsing fixes to conversation context length * remove unused import * update windows install scripts * zephyr * </s> stopping string * dialog cleanup utils improved * add agents and clients key to the config example
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@
|
||||
*-internal*
|
||||
*.internal*
|
||||
*_internal*
|
||||
talemate_env
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
agents: {}
|
||||
clients: {}
|
||||
creator:
|
||||
content_context:
|
||||
- a fun and engaging slice of life story aimed at an adult audience.
|
||||
|
||||
187
docs/talemate-scene-schema.json
Normal file
187
docs/talemate-scene-schema.json
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"intro": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"typ": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["message", "id", "typ", "source"]
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "string"
|
||||
},
|
||||
"archived_history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"ts": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text", "ts"]
|
||||
}
|
||||
},
|
||||
"character_states": {
|
||||
"type": "object"
|
||||
},
|
||||
"characters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"greeting_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"base_attributes": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
},
|
||||
"gender": {
|
||||
"type": "string"
|
||||
},
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"example_dialogue": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"history_events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"is_player": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cover_image": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "greeting_text", "base_attributes", "details", "gender", "color", "example_dialogue", "history_events", "is_player", "cover_image"]
|
||||
}
|
||||
},
|
||||
"goal": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"goals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"type": "string"
|
||||
},
|
||||
"world_state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"characters": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshot": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"emotion": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["snapshot", "emotion"]
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshot": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["snapshot"]
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["characters", "items", "location"]
|
||||
},
|
||||
"assets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cover_image": {
|
||||
"type": "string"
|
||||
},
|
||||
"assets": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"media_type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "file_type", "media_type"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["cover_image", "assets"]
|
||||
},
|
||||
"ts": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["description", "intro", "name", "history", "environment", "archived_history", "character_states", "characters", "context", "world_state", "assets", "ts"]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ REM activate the virtual environment
|
||||
call talemate_env\Scripts\activate
|
||||
|
||||
REM install poetry
|
||||
pip install poetry
|
||||
python -m pip install poetry "rapidfuzz>=3" -U
|
||||
|
||||
REM use poetry to install dependencies
|
||||
poetry install
|
||||
|
||||
2709
poetry.lock
generated
2709
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,8 @@ typing-inspect = "0.8.0"
|
||||
typing_extensions = "^4.5.0"
|
||||
uvicorn = "^0.23"
|
||||
blinker = "^1.6.2"
|
||||
pydantic = "<2"
|
||||
langchain = "0.0.213"
|
||||
pydantic = "<3"
|
||||
langchain = ">0.0.213"
|
||||
beautifulsoup4 = "^4.12.2"
|
||||
python-dotenv = "^1.0.0"
|
||||
websockets = "^11.0.3"
|
||||
@@ -36,6 +36,7 @@ structlog = "^23.1.0"
|
||||
runpod = "==1.2.0"
|
||||
nest_asyncio = "^1.5.7"
|
||||
isodate = ">=0.6.1"
|
||||
thefuzz = ">=0.20.0"
|
||||
|
||||
# ChromaDB
|
||||
chromadb = ">=0.4,<1"
|
||||
|
||||
18
reinstall.bat
Normal file
18
reinstall.bat
Normal file
@@ -0,0 +1,18 @@
|
||||
@echo off
|
||||
|
||||
IF EXIST talemate_env rmdir /s /q "talemate_env"
|
||||
|
||||
REM create a virtual environment
|
||||
python -m venv talemate_env
|
||||
|
||||
REM activate the virtual environment
|
||||
call talemate_env\Scripts\activate
|
||||
|
||||
REM install poetry
|
||||
python -m pip install poetry "rapidfuzz>=3" -U
|
||||
|
||||
REM use poetry to install dependencies
|
||||
python -m poetry install
|
||||
|
||||
echo Virtual environment re-created.
|
||||
pause
|
||||
@@ -6,4 +6,6 @@ from .director import DirectorAgent
|
||||
from .memory import ChromaDBMemoryAgent, MemoryAgent
|
||||
from .narrator import NarratorAgent
|
||||
from .registry import AGENT_CLASSES, get_agent_class, register
|
||||
from .summarize import SummarizeAgent
|
||||
from .summarize import SummarizeAgent
|
||||
from .editor import EditorAgent
|
||||
from .world_state import WorldStateAgent
|
||||
@@ -10,13 +10,30 @@ from blinker import signal
|
||||
import talemate.instance as instance
|
||||
import talemate.util as util
|
||||
from talemate.emit import emit
|
||||
|
||||
import dataclasses
|
||||
import pydantic
|
||||
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"set_processing",
|
||||
]
|
||||
|
||||
class AgentActionConfig(pydantic.BaseModel):
|
||||
type: str
|
||||
label: str
|
||||
description: str = ""
|
||||
value: Union[int, float, str, bool]
|
||||
max: Union[int, float, None] = None
|
||||
min: Union[int, float, None] = None
|
||||
step: Union[int, float, None] = None
|
||||
|
||||
class AgentAction(pydantic.BaseModel):
|
||||
enabled: bool = True
|
||||
label: str
|
||||
description: str = ""
|
||||
config: Union[dict[str, AgentActionConfig], None] = None
|
||||
|
||||
|
||||
def set_processing(fn):
|
||||
"""
|
||||
decorator that emits the agent status as processing while the function
|
||||
@@ -45,7 +62,6 @@ class Agent(ABC):
|
||||
|
||||
agent_type = "agent"
|
||||
verbose_name = None
|
||||
|
||||
set_processing = set_processing
|
||||
|
||||
@property
|
||||
@@ -59,18 +75,13 @@ class Agent(ABC):
|
||||
def verbose_name(self):
|
||||
return self.agent_type.capitalize()
|
||||
|
||||
@classmethod
|
||||
def config_options(cls):
|
||||
return {
|
||||
"client": [name for name, _ in instance.client_instances()],
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
if not getattr(self.client, "enabled", True):
|
||||
return False
|
||||
|
||||
|
||||
if self.client.current_status in ["error", "warning"]:
|
||||
return False
|
||||
|
||||
@@ -79,10 +90,77 @@ class Agent(ABC):
|
||||
@property
|
||||
def status(self):
|
||||
if self.ready:
|
||||
if not self.enabled:
|
||||
return "disabled"
|
||||
return "idle" if getattr(self, "processing", 0) == 0 else "busy"
|
||||
else:
|
||||
return "uninitialized"
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
# by default, agents are enabled, an agent class that
|
||||
# is disableable should override this property
|
||||
return True
|
||||
|
||||
@property
|
||||
def disable(self):
|
||||
# by default, agents are enabled, an agent class that
|
||||
# is disableable should override this property to
|
||||
# disable the agent
|
||||
pass
|
||||
|
||||
@property
|
||||
def has_toggle(self):
|
||||
# by default, agents do not have toggles to enable / disable
|
||||
# an agent class that is disableable should override this property
|
||||
return False
|
||||
|
||||
@property
|
||||
def experimental(self):
|
||||
# by default, agents are not experimental, an agent class that
|
||||
# is experimental should override this property
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def config_options(cls, agent=None):
|
||||
config_options = {
|
||||
"client": [name for name, _ in instance.client_instances()],
|
||||
"enabled": agent.enabled if agent else True,
|
||||
"has_toggle": agent.has_toggle if agent else False,
|
||||
"experimental": agent.experimental if agent else False,
|
||||
}
|
||||
actions = getattr(agent, "actions", None)
|
||||
|
||||
if actions:
|
||||
config_options["actions"] = {k: v.model_dump() for k, v in actions.items()}
|
||||
else:
|
||||
config_options["actions"] = {}
|
||||
|
||||
return config_options
|
||||
|
||||
def apply_config(self, *args, **kwargs):
|
||||
if self.has_toggle and "enabled" in kwargs:
|
||||
self.is_enabled = kwargs.get("enabled", False)
|
||||
|
||||
if not getattr(self, "actions", None):
|
||||
return
|
||||
|
||||
for action_key, action in self.actions.items():
|
||||
|
||||
if not kwargs.get("actions"):
|
||||
continue
|
||||
|
||||
action.enabled = kwargs.get("actions", {}).get(action_key, {}).get("enabled", False)
|
||||
|
||||
if not action.config:
|
||||
continue
|
||||
|
||||
for config_key, config in action.config.items():
|
||||
try:
|
||||
config.value = kwargs.get("actions", {}).get(action_key, {}).get("config", {}).get(config_key, {}).get("value", config.value)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
async def emit_status(self, processing: bool = None):
|
||||
|
||||
# should keep a count of processing requests, and when the
|
||||
@@ -101,6 +179,8 @@ class Agent(ABC):
|
||||
self.processing += 1
|
||||
|
||||
status = "busy" if self.processing > 0 else "idle"
|
||||
if not self.enabled:
|
||||
status = "disabled"
|
||||
|
||||
emit(
|
||||
"agent_status",
|
||||
@@ -108,7 +188,7 @@ class Agent(ABC):
|
||||
id=self.agent_type,
|
||||
status=status,
|
||||
details=self.agent_details,
|
||||
data=self.config_options(),
|
||||
data=self.config_options(agent=self),
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
@@ -159,3 +239,7 @@ class Agent(ABC):
|
||||
|
||||
current_memory_context.append(memory)
|
||||
return current_memory_context
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AgentEmission:
|
||||
agent: Agent
|
||||
@@ -1,24 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import talemate.client as client
|
||||
import talemate.util as util
|
||||
import structlog
|
||||
from talemate.emit import emit
|
||||
import talemate.emit.async_signals
|
||||
from talemate.scene_message import CharacterMessage, DirectorMessage
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.events import GameLoopEvent
|
||||
from talemate.client.context import set_conversation_context_attribute, client_context_attribute, set_client_context_attribute
|
||||
|
||||
from .base import Agent, set_processing
|
||||
from .base import Agent, AgentEmission, set_processing, AgentAction, AgentActionConfig
|
||||
from .registry import register
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from talemate.tale_mate import Character, Scene
|
||||
from talemate.tale_mate import Character, Scene, Actor
|
||||
|
||||
log = structlog.get_logger("talemate.agents.conversation")
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ConversationAgentEmission(AgentEmission):
|
||||
actor: Actor
|
||||
character: Character
|
||||
generation: list[str]
|
||||
|
||||
talemate.emit.async_signals.register(
|
||||
"agent.conversation.generated"
|
||||
)
|
||||
|
||||
@register()
|
||||
class ConversationAgent(Agent):
|
||||
"""
|
||||
@@ -44,7 +58,223 @@ class ConversationAgent(Agent):
|
||||
self.logging_enabled = logging_enabled
|
||||
self.logging_date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
self.current_memory_context = None
|
||||
|
||||
# several agents extend this class, but we only want to initialize
|
||||
# these actions for the conversation agent
|
||||
|
||||
if self.agent_type != "conversation":
|
||||
return
|
||||
|
||||
self.actions = {
|
||||
"generation_override": AgentAction(
|
||||
enabled = True,
|
||||
label = "Generation Override",
|
||||
description = "Override generation parameters",
|
||||
config = {
|
||||
"length": AgentActionConfig(
|
||||
type="number",
|
||||
label="Generation Length (tokens)",
|
||||
description="Maximum number of tokens to generate for a conversation response.",
|
||||
value=96,
|
||||
min=32,
|
||||
max=512,
|
||||
step=32,
|
||||
),
|
||||
"jiggle": AgentActionConfig(
|
||||
type="number",
|
||||
label="Jiggle",
|
||||
description="If > 0.0 will cause certain generation parameters to have a slight random offset applied to them. The bigger the number, the higher the potential offset.",
|
||||
value=0.0,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
step=0.1,
|
||||
),
|
||||
}
|
||||
),
|
||||
"natural_flow": AgentAction(
|
||||
enabled = True,
|
||||
label = "Natural Flow",
|
||||
description = "Will attempt to generate a more natural flow of conversation between multiple characters.",
|
||||
config = {
|
||||
"max_auto_turns": AgentActionConfig(
|
||||
type="number",
|
||||
label="Max. Auto Turns",
|
||||
description="The maximum number of turns the AI is allowed to generate before it stops and waits for the player to respond.",
|
||||
value=4,
|
||||
min=1,
|
||||
max=100,
|
||||
step=1,
|
||||
),
|
||||
"max_idle_turns": AgentActionConfig(
|
||||
type="number",
|
||||
label="Max. Idle Turns",
|
||||
description="The maximum number of turns a character can go without speaking before they are considered overdue to speak.",
|
||||
value=8,
|
||||
min=1,
|
||||
max=100,
|
||||
step=1,
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
def connect(self, scene):
|
||||
super().connect(scene)
|
||||
talemate.emit.async_signals.get("game_loop").connect(self.on_game_loop)
|
||||
|
||||
def last_spoken(self):
|
||||
|
||||
"""
|
||||
Returns the last time each character spoke
|
||||
"""
|
||||
|
||||
last_turn = {}
|
||||
turns = 0
|
||||
character_names = self.scene.character_names
|
||||
max_idle_turns = self.actions["natural_flow"].config["max_idle_turns"].value
|
||||
|
||||
for idx in range(len(self.scene.history) - 1, -1, -1):
|
||||
|
||||
if isinstance(self.scene.history[idx], CharacterMessage):
|
||||
|
||||
if turns >= max_idle_turns:
|
||||
break
|
||||
|
||||
character = self.scene.history[idx].character_name
|
||||
|
||||
if character in character_names:
|
||||
last_turn[character] = turns
|
||||
character_names.remove(character)
|
||||
|
||||
if not character_names:
|
||||
break
|
||||
|
||||
turns += 1
|
||||
|
||||
if character_names and turns >= max_idle_turns:
|
||||
for character in character_names:
|
||||
last_turn[character] = max_idle_turns
|
||||
|
||||
return last_turn
|
||||
|
||||
def repeated_speaker(self):
|
||||
"""
|
||||
Counts the amount of times the most recent speaker has spoken in a row
|
||||
"""
|
||||
character_name = None
|
||||
count = 0
|
||||
for idx in range(len(self.scene.history) - 1, -1, -1):
|
||||
if isinstance(self.scene.history[idx], CharacterMessage):
|
||||
if character_name is None:
|
||||
character_name = self.scene.history[idx].character_name
|
||||
if self.scene.history[idx].character_name == character_name:
|
||||
count += 1
|
||||
else:
|
||||
break
|
||||
return count
|
||||
|
||||
async def on_game_loop(self, event:GameLoopEvent):
|
||||
await self.apply_natural_flow()
|
||||
|
||||
async def apply_natural_flow(self):
|
||||
"""
|
||||
If the natural flow action is enabled, this will attempt to determine
|
||||
the ideal character to talk next.
|
||||
|
||||
This will let the AI pick a character to talk to, but if the AI can't figure
|
||||
it out it will apply rules based on max_idle_turns and max_auto_turns.
|
||||
|
||||
If all fails it will just pick a random character.
|
||||
|
||||
Repetition is also taken into account, so if a character has spoken twice in a row
|
||||
they will not be picked again until someone else has spoken.
|
||||
"""
|
||||
|
||||
scene = self.scene
|
||||
if self.actions["natural_flow"].enabled and len(scene.character_names) > 2:
|
||||
|
||||
# last time each character spoke (turns ago)
|
||||
max_idle_turns = self.actions["natural_flow"].config["max_idle_turns"].value
|
||||
max_auto_turns = self.actions["natural_flow"].config["max_auto_turns"].value
|
||||
last_turn = self.last_spoken()
|
||||
last_turn_player = last_turn.get(scene.get_player_character().name, 0)
|
||||
|
||||
if last_turn_player >= max_auto_turns:
|
||||
self.scene.next_actor = scene.get_player_character().name
|
||||
log.debug("conversation_agent.natural_flow", next_actor="player", overdue=True, player_character=scene.get_player_character().name)
|
||||
return
|
||||
|
||||
log.debug("conversation_agent.natural_flow", last_turn=last_turn)
|
||||
|
||||
# determine random character to talk, this will be the fallback in case
|
||||
# the AI can't figure out who should talk next
|
||||
|
||||
if scene.prev_actor:
|
||||
|
||||
# we dont want to talk to the same person twice in a row
|
||||
character_names = scene.character_names
|
||||
character_names.remove(scene.prev_actor)
|
||||
random_character_name = random.choice(character_names)
|
||||
else:
|
||||
character_names = scene.character_names
|
||||
# no one has talked yet, so we just pick a random character
|
||||
|
||||
random_character_name = random.choice(scene.character_names)
|
||||
|
||||
overdue_characters = [character for character, turn in last_turn.items() if turn >= max_idle_turns]
|
||||
|
||||
if overdue_characters and self.scene.history:
|
||||
# Pick a random character from the overdue characters
|
||||
scene.next_actor = random.choice(overdue_characters)
|
||||
elif scene.history:
|
||||
scene.next_actor = None
|
||||
|
||||
# AI will attempt to figure out who should talk next
|
||||
next_actor = await self.select_talking_actor(character_names)
|
||||
next_actor = next_actor.strip().strip('"').strip(".")
|
||||
|
||||
for character_name in scene.character_names:
|
||||
if next_actor.lower() in character_name.lower() or character_name.lower() in next_actor.lower():
|
||||
scene.next_actor = character_name
|
||||
break
|
||||
|
||||
if not scene.next_actor:
|
||||
# AI couldn't figure out who should talk next, so we just pick a random character
|
||||
log.debug("conversation_agent.natural_flow", next_actor="random", random_character_name=random_character_name)
|
||||
scene.next_actor = random_character_name
|
||||
else:
|
||||
log.debug("conversation_agent.natural_flow", next_actor="picked", ai_next_actor=scene.next_actor)
|
||||
else:
|
||||
# always start with main character (TODO: configurable?)
|
||||
player_character = scene.get_player_character()
|
||||
log.debug("conversation_agent.natural_flow", next_actor="main_character", main_character=player_character)
|
||||
scene.next_actor = player_character.name if player_character else random_character_name
|
||||
|
||||
scene.log.debug("conversation_agent.natural_flow", next_actor=scene.next_actor)
|
||||
|
||||
|
||||
# same character cannot go thrice in a row, if this is happening, pick a random character that
|
||||
# isnt the same as the last character
|
||||
|
||||
if self.repeated_speaker() >= 2 and self.scene.prev_actor == self.scene.next_actor:
|
||||
scene.next_actor = random.choice([c for c in scene.character_names if c != scene.prev_actor])
|
||||
scene.log.debug("conversation_agent.natural_flow", next_actor="random (repeated safeguard)", random_character_name=scene.next_actor)
|
||||
|
||||
else:
|
||||
scene.next_actor = None
|
||||
|
||||
|
||||
@set_processing
|
||||
async def select_talking_actor(self, character_names: list[str]=None):
|
||||
result = await Prompt.request("conversation.select-talking-actor", self.client, "conversation_select_talking_actor", vars={
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"character_names": character_names or self.scene.character_names,
|
||||
"character_names_formatted": ", ".join(character_names or self.scene.character_names),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def build_prompt_default(
|
||||
self,
|
||||
@@ -158,24 +388,30 @@ class ConversationAgent(Agent):
|
||||
|
||||
def clean_result(self, result, character):
|
||||
|
||||
log.debug("clean result", result=result)
|
||||
|
||||
if "#" in result:
|
||||
result = result.split("#")[0]
|
||||
|
||||
result = result.replace(" :", ":")
|
||||
result = result.strip().strip('"').strip()
|
||||
result = result.replace("[", "*").replace("]", "*")
|
||||
result = result.replace("(", "*").replace(")", "*")
|
||||
result = result.replace("**", "*")
|
||||
|
||||
# if there is an uneven number of '*' add one to the end
|
||||
|
||||
if result.count("*") % 2 == 1:
|
||||
result += "*"
|
||||
|
||||
return result
|
||||
|
||||
def set_generation_overrides(self):
|
||||
if not self.actions["generation_override"].enabled:
|
||||
return
|
||||
|
||||
set_conversation_context_attribute("length", self.actions["generation_override"].config["length"].value)
|
||||
|
||||
if self.actions["generation_override"].config["jiggle"].value > 0.0:
|
||||
nuke_repetition = client_context_attribute("nuke_repetition")
|
||||
if nuke_repetition == 0.0:
|
||||
# we only apply the agent override if some other mechanism isn't already
|
||||
# setting the nuke_repetition value
|
||||
nuke_repetition = self.actions["generation_override"].config["jiggle"].value
|
||||
set_client_context_attribute("nuke_repetition", nuke_repetition)
|
||||
|
||||
@set_processing
|
||||
async def converse(self, actor, editor=None):
|
||||
"""
|
||||
@@ -186,6 +422,8 @@ class ConversationAgent(Agent):
|
||||
self.current_memory_context = None
|
||||
|
||||
character = actor.character
|
||||
|
||||
self.set_generation_overrides()
|
||||
|
||||
result = await self.client.send_prompt(await self.build_prompt(character))
|
||||
|
||||
@@ -230,7 +468,7 @@ class ConversationAgent(Agent):
|
||||
total_result = total_result.split("#")[0]
|
||||
|
||||
# Removes partial sentence at the end
|
||||
total_result = util.strip_partial_sentences(total_result)
|
||||
total_result = util.clean_dialogue(total_result, main_name=character.name)
|
||||
|
||||
# Remove "{character.name}:" - all occurences
|
||||
total_result = total_result.replace(f"{character.name}:", "")
|
||||
@@ -253,13 +491,15 @@ class ConversationAgent(Agent):
|
||||
)
|
||||
|
||||
response_message = util.parse_messages_from_str(total_result, [character.name])
|
||||
|
||||
log.info("conversation agent", result=response_message)
|
||||
|
||||
emission = ConversationAgentEmission(agent=self, generation=response_message, actor=actor, character=character)
|
||||
await talemate.emit.async_signals.get("agent.conversation.generated").send(emission)
|
||||
|
||||
if editor:
|
||||
response_message = [
|
||||
editor.help_edit(character, message) for message in response_message
|
||||
]
|
||||
#log.info("conversation agent", generation=emission.generation)
|
||||
|
||||
messages = [CharacterMessage(message) for message in response_message]
|
||||
messages = [CharacterMessage(message) for message in emission.generation]
|
||||
|
||||
# Add message and response to conversation history
|
||||
actor.scene.push_history(messages)
|
||||
|
||||
@@ -3,15 +3,16 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
|
||||
from talemate.agents.conversation import ConversationAgent
|
||||
from talemate.agents.base import Agent
|
||||
from talemate.agents.registry import register
|
||||
from talemate.emit import emit
|
||||
import talemate.client as client
|
||||
|
||||
from .character import CharacterCreatorMixin
|
||||
from .scenario import ScenarioCreatorMixin
|
||||
|
||||
@register()
|
||||
class CreatorAgent(CharacterCreatorMixin, ScenarioCreatorMixin, ConversationAgent):
|
||||
class CreatorAgent(CharacterCreatorMixin, ScenarioCreatorMixin, Agent):
|
||||
|
||||
"""
|
||||
Creates characters and scenarios and other fun stuff!
|
||||
@@ -20,6 +21,13 @@ class CreatorAgent(CharacterCreatorMixin, ScenarioCreatorMixin, ConversationAgen
|
||||
agent_type = "creator"
|
||||
verbose_name = "Creator"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: client.TaleMateClient,
|
||||
**kwargs,
|
||||
):
|
||||
self.client = client
|
||||
|
||||
def clean_result(self, result):
|
||||
if "#" in result:
|
||||
result = result.split("#")[0]
|
||||
|
||||
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Callable
|
||||
import talemate.util as util
|
||||
from talemate.emit import emit
|
||||
from talemate.prompts import Prompt, LoopedPrompt
|
||||
from talemate.exceptions import LLMAccuracyError
|
||||
from talemate.agents.base import set_processing
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from talemate.tale_mate import Character
|
||||
@@ -19,7 +21,11 @@ def validate(k,v):
|
||||
if k and k.lower() == "gender":
|
||||
return v.lower().strip()
|
||||
if k and k.lower() == "age":
|
||||
return int(v.strip())
|
||||
try:
|
||||
return int(v.split("\n")[0].strip())
|
||||
except (ValueError, TypeError):
|
||||
raise LLMAccuracyError("Was unable to get a valid age from the response", model_name=None)
|
||||
|
||||
return v.strip().strip("\n")
|
||||
|
||||
DEFAULT_CONTENT_CONTEXT="a fun and engaging adventure aimed at an adult audience."
|
||||
@@ -31,6 +37,7 @@ class CharacterCreatorMixin:
|
||||
|
||||
## NEW
|
||||
|
||||
@set_processing
|
||||
async def create_character_attributes(
|
||||
self,
|
||||
character_prompt: str,
|
||||
@@ -42,60 +49,55 @@ class CharacterCreatorMixin:
|
||||
predefined_attributes: dict[str, str] = dict(),
|
||||
):
|
||||
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
|
||||
def spice(prompt, spices):
|
||||
# generate number from 0 to 1 and if its smaller than use_spice
|
||||
# select a random spice from the list and return it formatted
|
||||
# in the prompt
|
||||
if random.random() < use_spice:
|
||||
spice = random.choice(spices)
|
||||
return prompt.format(spice=spice)
|
||||
return ""
|
||||
|
||||
# drop any empty attributes from predefined_attributes
|
||||
|
||||
predefined_attributes = {k:v for k,v in predefined_attributes.items() if v}
|
||||
|
||||
prompt = Prompt.get(f"creator.character-attributes-{template}", vars={
|
||||
"character_prompt": character_prompt,
|
||||
"template": template,
|
||||
"spice": spice,
|
||||
"content_context": content_context,
|
||||
"custom_attributes": custom_attributes,
|
||||
"character_sheet": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=attribute_callback,
|
||||
generated=predefined_attributes,
|
||||
),
|
||||
})
|
||||
await prompt.loop(self.client, "character_sheet", kind="create_concise")
|
||||
|
||||
return prompt.vars["character_sheet"].generated
|
||||
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
def spice(prompt, spices):
|
||||
# generate number from 0 to 1 and if its smaller than use_spice
|
||||
# select a random spice from the list and return it formatted
|
||||
# in the prompt
|
||||
if random.random() < use_spice:
|
||||
spice = random.choice(spices)
|
||||
return prompt.format(spice=spice)
|
||||
return ""
|
||||
|
||||
# drop any empty attributes from predefined_attributes
|
||||
|
||||
predefined_attributes = {k:v for k,v in predefined_attributes.items() if v}
|
||||
|
||||
prompt = Prompt.get(f"creator.character-attributes-{template}", vars={
|
||||
"character_prompt": character_prompt,
|
||||
"template": template,
|
||||
"spice": spice,
|
||||
"content_context": content_context,
|
||||
"custom_attributes": custom_attributes,
|
||||
"character_sheet": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=attribute_callback,
|
||||
generated=predefined_attributes,
|
||||
),
|
||||
})
|
||||
await prompt.loop(self.client, "character_sheet", kind="create_concise")
|
||||
|
||||
return prompt.vars["character_sheet"].generated
|
||||
|
||||
|
||||
|
||||
@set_processing
|
||||
async def create_character_description(
|
||||
self,
|
||||
character:Character,
|
||||
content_context: str = DEFAULT_CONTENT_CONTEXT,
|
||||
):
|
||||
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
description = await Prompt.request(f"creator.character-description", self.client, "create", vars={
|
||||
"character": character,
|
||||
"content_context": content_context,
|
||||
})
|
||||
|
||||
return description.strip()
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
description = await Prompt.request(f"creator.character-description", self.client, "create", vars={
|
||||
"character": character,
|
||||
"content_context": content_context,
|
||||
})
|
||||
|
||||
return description.strip()
|
||||
|
||||
|
||||
|
||||
@set_processing
|
||||
async def create_character_details(
|
||||
self,
|
||||
character: Character,
|
||||
@@ -104,23 +106,21 @@ class CharacterCreatorMixin:
|
||||
questions: list[str] = None,
|
||||
content_context: str = DEFAULT_CONTENT_CONTEXT,
|
||||
):
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
prompt = Prompt.get(f"creator.character-details-{template}", vars={
|
||||
"character_details": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=detail_callback,
|
||||
),
|
||||
"template": template,
|
||||
"content_context": content_context,
|
||||
"character": character,
|
||||
"custom_questions": questions or [],
|
||||
})
|
||||
await prompt.loop(self.client, "character_details", kind="create_concise")
|
||||
return prompt.vars["character_details"].generated
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
prompt = Prompt.get(f"creator.character-details-{template}", vars={
|
||||
"character_details": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=detail_callback,
|
||||
),
|
||||
"template": template,
|
||||
"content_context": content_context,
|
||||
"character": character,
|
||||
"custom_questions": questions or [],
|
||||
})
|
||||
await prompt.loop(self.client, "character_details", kind="create_concise")
|
||||
return prompt.vars["character_details"].generated
|
||||
|
||||
|
||||
@set_processing
|
||||
async def create_character_example_dialogue(
|
||||
self,
|
||||
character: Character,
|
||||
@@ -132,64 +132,86 @@ class CharacterCreatorMixin:
|
||||
rules_callback: Callable = lambda rules: None,
|
||||
):
|
||||
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
dialogue_rules = await Prompt.request(f"creator.character-dialogue-rules", self.client, "create", vars={
|
||||
"guide": guide,
|
||||
"character": character,
|
||||
"examples": examples or [],
|
||||
"content_context": content_context,
|
||||
})
|
||||
|
||||
dialogue_rules = await Prompt.request(f"creator.character-dialogue-rules", self.client, "create", vars={
|
||||
"guide": guide,
|
||||
"character": character,
|
||||
"examples": examples or [],
|
||||
"content_context": content_context,
|
||||
})
|
||||
log.info("dialogue_rules", dialogue_rules=dialogue_rules)
|
||||
|
||||
if rules_callback:
|
||||
rules_callback(dialogue_rules)
|
||||
|
||||
log.info("dialogue_rules", dialogue_rules=dialogue_rules)
|
||||
|
||||
if rules_callback:
|
||||
rules_callback(dialogue_rules)
|
||||
|
||||
example_dialogue_prompt = Prompt.get(f"creator.character-example-dialogue-{template}", vars={
|
||||
"guide": guide,
|
||||
"character": character,
|
||||
"examples": examples or [],
|
||||
"content_context": content_context,
|
||||
"dialogue_rules": dialogue_rules,
|
||||
"generated_examples": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=example_callback,
|
||||
),
|
||||
})
|
||||
|
||||
await example_dialogue_prompt.loop(self.client, "generated_examples", kind="create")
|
||||
|
||||
return example_dialogue_prompt.vars["generated_examples"].generated
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
example_dialogue_prompt = Prompt.get(f"creator.character-example-dialogue-{template}", vars={
|
||||
"guide": guide,
|
||||
"character": character,
|
||||
"examples": examples or [],
|
||||
"content_context": content_context,
|
||||
"dialogue_rules": dialogue_rules,
|
||||
"generated_examples": LoopedPrompt(
|
||||
validate_value=validate,
|
||||
on_update=example_callback,
|
||||
),
|
||||
})
|
||||
|
||||
await example_dialogue_prompt.loop(self.client, "generated_examples", kind="create")
|
||||
|
||||
return example_dialogue_prompt.vars["generated_examples"].generated
|
||||
|
||||
|
||||
|
||||
@set_processing
|
||||
async def determine_content_context_for_character(
|
||||
self,
|
||||
character: Character,
|
||||
):
|
||||
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
content_context = await Prompt.request(f"creator.determine-content-context", self.client, "create", vars={
|
||||
"character": character,
|
||||
})
|
||||
return content_context.strip()
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
content_context = await Prompt.request(f"creator.determine-content-context", self.client, "create", vars={
|
||||
"character": character,
|
||||
})
|
||||
return content_context.strip()
|
||||
|
||||
|
||||
@set_processing
|
||||
async def determine_character_attributes(
|
||||
self,
|
||||
character: Character,
|
||||
):
|
||||
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
attributes = await Prompt.request(f"creator.determine-character-attributes", self.client, "analyze_long", vars={
|
||||
"character": character,
|
||||
})
|
||||
return attributes
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
attributes = await Prompt.request(f"creator.determine-character-attributes", self.client, "analyze_long", vars={
|
||||
"character": character,
|
||||
})
|
||||
return attributes
|
||||
|
||||
@set_processing
|
||||
async def determine_character_description(
|
||||
self,
|
||||
character: Character,
|
||||
text:str=""
|
||||
):
|
||||
|
||||
description = await Prompt.request(f"creator.determine-character-description", self.client, "create", vars={
|
||||
"character": character,
|
||||
"scene": self.scene,
|
||||
"text": text,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
})
|
||||
return description.strip()
|
||||
|
||||
@set_processing
|
||||
async def generate_character_from_text(
|
||||
self,
|
||||
text: str,
|
||||
template: str,
|
||||
content_context: str = DEFAULT_CONTENT_CONTEXT,
|
||||
):
|
||||
|
||||
base_attributes = await self.create_character_attributes(
|
||||
character_prompt=text,
|
||||
template=template,
|
||||
content_context=content_context,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import re
|
||||
import random
|
||||
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.agents.base import set_processing
|
||||
|
||||
class ScenarioCreatorMixin:
|
||||
|
||||
@@ -10,8 +11,7 @@ class ScenarioCreatorMixin:
|
||||
Adds scenario creation functionality to the creator agent
|
||||
"""
|
||||
|
||||
### NEW
|
||||
|
||||
@set_processing
|
||||
async def create_scene_description(
|
||||
self,
|
||||
prompt:str,
|
||||
@@ -29,27 +29,23 @@ class ScenarioCreatorMixin:
|
||||
|
||||
callback (callable): A callback to call when the scene has been created.
|
||||
"""
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
scene = self.scene
|
||||
scene = self.scene
|
||||
|
||||
description = await Prompt.request(
|
||||
"creator.scenario-description",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
description = description.strip()
|
||||
|
||||
return description
|
||||
|
||||
description = await Prompt.request(
|
||||
"creator.scenario-description",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
description = description.strip()
|
||||
|
||||
return description
|
||||
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
|
||||
|
||||
async def create_scene_name(
|
||||
@@ -70,27 +66,21 @@ class ScenarioCreatorMixin:
|
||||
|
||||
description (str): The description of the scene.
|
||||
"""
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
|
||||
scene = self.scene
|
||||
|
||||
name = await Prompt.request(
|
||||
"creator.scenario-name",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"description": description,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
name = name.strip().strip('.!').replace('"','')
|
||||
return name
|
||||
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
scene = self.scene
|
||||
|
||||
name = await Prompt.request(
|
||||
"creator.scenario-name",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"description": description,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
name = name.strip().strip('.!').replace('"','')
|
||||
return name
|
||||
|
||||
|
||||
async def create_scene_intro(
|
||||
@@ -114,25 +104,30 @@ class ScenarioCreatorMixin:
|
||||
|
||||
name (str): The name of the scene.
|
||||
"""
|
||||
try:
|
||||
await self.emit_status(processing=True)
|
||||
|
||||
scene = self.scene
|
||||
|
||||
intro = await Prompt.request(
|
||||
"creator.scenario-intro",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"description": description,
|
||||
"name": name,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
intro = intro.strip()
|
||||
return intro
|
||||
|
||||
finally:
|
||||
await self.emit_status(processing=False)
|
||||
|
||||
scene = self.scene
|
||||
|
||||
intro = await Prompt.request(
|
||||
"creator.scenario-intro",
|
||||
self.client,
|
||||
"create",
|
||||
vars={
|
||||
"prompt": prompt,
|
||||
"content_context": content_context,
|
||||
"description": description,
|
||||
"name": name,
|
||||
"scene": scene,
|
||||
}
|
||||
)
|
||||
intro = intro.strip()
|
||||
return intro
|
||||
|
||||
@set_processing
|
||||
async def determine_scenario_description(
|
||||
self,
|
||||
text:str
|
||||
):
|
||||
description = await Prompt.request(f"creator.determine-scenario-description", self.client, "analyze_long", vars={
|
||||
"text": text,
|
||||
})
|
||||
return description
|
||||
|
||||
@@ -12,9 +12,8 @@ from talemate.prompts import Prompt
|
||||
from talemate.scene_message import NarratorMessage, DirectorMessage
|
||||
from talemate.automated_action import AutomatedAction
|
||||
import talemate.automated_action as automated_action
|
||||
from .conversation import ConversationAgent
|
||||
from .registry import register
|
||||
from .base import set_processing
|
||||
from .base import set_processing, AgentAction, AgentActionConfig, Agent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from talemate import Actor, Character, Player, Scene
|
||||
@@ -22,10 +21,31 @@ if TYPE_CHECKING:
|
||||
log = structlog.get_logger("talemate")
|
||||
|
||||
@register()
|
||||
class DirectorAgent(ConversationAgent):
|
||||
class DirectorAgent(Agent):
|
||||
agent_type = "director"
|
||||
verbose_name = "Director"
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
self.is_enabled = True
|
||||
self.client = client
|
||||
self.actions = {
|
||||
"direct": AgentAction(enabled=False, label="Direct", description="Will attempt to direct the scene. Runs automatically after AI dialogue (n turns).", config={
|
||||
"turns": AgentActionConfig(type="number", label="Turns", description="Number of turns to wait before directing the sceen", value=10, min=1, max=100, step=1)
|
||||
}),
|
||||
}
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self.is_enabled
|
||||
|
||||
@property
|
||||
def has_toggle(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def experimental(self):
|
||||
return True
|
||||
|
||||
def get_base_prompt(self, character: Character, budget:int):
|
||||
return [character.description, character.base_attributes.get("scenario_context", "")] + self.scene.context_history(budget=budget, keep_director=False)
|
||||
|
||||
@@ -338,34 +358,4 @@ class DirectorAgent(ConversationAgent):
|
||||
else:
|
||||
goal_met = True
|
||||
|
||||
return goal_met
|
||||
|
||||
|
||||
@automated_action.register("director", frequency=4, call_initially=True, enabled=False)
|
||||
class AutomatedDirector(automated_action.AutomatedAction):
|
||||
"""
|
||||
Runs director.direct actions every n turns
|
||||
"""
|
||||
|
||||
async def action(self):
|
||||
scene = self.scene
|
||||
director = scene.get_helper("director")
|
||||
|
||||
if not scene.active_actor or scene.active_actor.character.is_player:
|
||||
return False
|
||||
|
||||
if not director:
|
||||
return
|
||||
|
||||
director_response = await director.agent.direct(scene.active_actor.character)
|
||||
|
||||
if director_response is True:
|
||||
# director directed different agent, nothing to do
|
||||
return
|
||||
|
||||
if not director_response:
|
||||
return
|
||||
|
||||
director_message = DirectorMessage(director_response, source=scene.active_actor.character.name)
|
||||
emit("director", director_message, character=scene.active_actor.character)
|
||||
scene.push_history(director_message)
|
||||
return goal_met
|
||||
163
src/talemate/agents/editor.py
Normal file
163
src/talemate/agents/editor.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Union
|
||||
|
||||
import talemate.data_objects as data_objects
|
||||
import talemate.util as util
|
||||
import talemate.emit.async_signals
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.scene_message import DirectorMessage, TimePassageMessage
|
||||
|
||||
from .base import Agent, set_processing, AgentAction
|
||||
from .registry import register
|
||||
|
||||
import structlog
|
||||
|
||||
import time
|
||||
import re
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from talemate.tale_mate import Actor, Character, Scene
|
||||
from talemate.agents.conversation import ConversationAgentEmission
|
||||
|
||||
log = structlog.get_logger("talemate.agents.editor")
|
||||
|
||||
@register()
|
||||
class EditorAgent(Agent):
|
||||
"""
|
||||
Editor agent
|
||||
|
||||
will attempt to improve the quality of dialogue
|
||||
"""
|
||||
|
||||
agent_type = "editor"
|
||||
verbose_name = "Editor"
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
self.client = client
|
||||
self.is_enabled = True
|
||||
self.actions = {
|
||||
"edit_dialogue": AgentAction(enabled=False, label="Edit dialogue", description="Will attempt to improve the quality of dialogue based on the character and scene. Runs automatically after each AI dialogue."),
|
||||
"fix_exposition": AgentAction(enabled=True, label="Fix exposition", description="Will attempt to fix exposition and emotes, making sure they are displayed in italics. Runs automatically after each AI dialogue."),
|
||||
"add_detail": AgentAction(enabled=False, label="Add detail", description="Will attempt to add extra detail and exposition to the dialogue. Runs automatically after each AI dialogue.")
|
||||
}
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self.is_enabled
|
||||
|
||||
@property
|
||||
def has_toggle(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def experimental(self):
|
||||
return True
|
||||
|
||||
def connect(self, scene):
|
||||
super().connect(scene)
|
||||
talemate.emit.async_signals.get("agent.conversation.generated").connect(self.on_conversation_generated)
|
||||
|
||||
async def on_conversation_generated(self, emission:ConversationAgentEmission):
|
||||
"""
|
||||
Called when a conversation is generated
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
log.info("editing conversation", emission=emission)
|
||||
|
||||
edited = []
|
||||
for text in emission.generation:
|
||||
|
||||
|
||||
edit = await self.add_detail(
|
||||
text,
|
||||
emission.character
|
||||
)
|
||||
|
||||
edit = await self.edit_conversation(
|
||||
edit,
|
||||
emission.character
|
||||
)
|
||||
|
||||
edit = await self.fix_exposition(
|
||||
edit,
|
||||
emission.character
|
||||
)
|
||||
|
||||
edited.append(edit)
|
||||
|
||||
emission.generation = edited
|
||||
|
||||
|
||||
@set_processing
|
||||
async def edit_conversation(self, content:str, character:Character):
|
||||
"""
|
||||
Edits a conversation
|
||||
"""
|
||||
|
||||
if not self.actions["edit_dialogue"].enabled:
|
||||
return content
|
||||
|
||||
response = await Prompt.request("editor.edit-dialogue", self.client, "edit_dialogue", vars={
|
||||
"content": content,
|
||||
"character": character,
|
||||
"scene": self.scene,
|
||||
"max_length": self.client.max_token_length
|
||||
})
|
||||
|
||||
response = response.split("[end]")[0]
|
||||
|
||||
response = util.replace_exposition_markers(response)
|
||||
response = util.clean_dialogue(response, main_name=character.name)
|
||||
response = util.strip_partial_sentences(response)
|
||||
|
||||
return response
|
||||
|
||||
@set_processing
|
||||
async def fix_exposition(self, content:str, character:Character):
|
||||
"""
|
||||
Edits a text to make sure all narrative exposition and emotes is encased in *
|
||||
"""
|
||||
|
||||
if not self.actions["fix_exposition"].enabled:
|
||||
return content
|
||||
|
||||
#response = await Prompt.request("editor.fix-exposition", self.client, "edit_fix_exposition", vars={
|
||||
# "content": content,
|
||||
# "character": character,
|
||||
# "scene": self.scene,
|
||||
# "max_length": self.client.max_token_length
|
||||
#})
|
||||
|
||||
content = util.clean_dialogue(content, main_name=character.name)
|
||||
content = util.strip_partial_sentences(content)
|
||||
content = util.ensure_dialog_format(content, talking_character=character.name)
|
||||
|
||||
return content
|
||||
|
||||
@set_processing
|
||||
async def add_detail(self, content:str, character:Character):
|
||||
"""
|
||||
Edits a text to increase its length and add extra detail and exposition
|
||||
"""
|
||||
|
||||
if not self.actions["add_detail"].enabled:
|
||||
return content
|
||||
|
||||
response = await Prompt.request("editor.add-detail", self.client, "edit_add_detail", vars={
|
||||
"content": content,
|
||||
"character": character,
|
||||
"scene": self.scene,
|
||||
"max_length": self.client.max_token_length
|
||||
})
|
||||
|
||||
response = util.replace_exposition_markers(response)
|
||||
response = util.clean_dialogue(response, main_name=character.name)
|
||||
response = util.strip_partial_sentences(response)
|
||||
|
||||
return response
|
||||
@@ -35,7 +35,7 @@ class MemoryAgent(Agent):
|
||||
verbose_name = "Long-term memory"
|
||||
|
||||
@classmethod
|
||||
def config_options(cls):
|
||||
def config_options(cls, agent=None):
|
||||
return {}
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
|
||||
@@ -7,20 +7,27 @@ from typing import TYPE_CHECKING, Callable, List, Optional, Union
|
||||
import talemate.util as util
|
||||
from talemate.emit import wait_for_input
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.agents.base import set_processing
|
||||
from talemate.agents.base import set_processing, Agent
|
||||
import talemate.client as client
|
||||
|
||||
from .conversation import ConversationAgent
|
||||
from .registry import register
|
||||
|
||||
|
||||
|
||||
|
||||
@register()
|
||||
class NarratorAgent(ConversationAgent):
|
||||
class NarratorAgent(Agent):
|
||||
agent_type = "narrator"
|
||||
verbose_name = "Narrator"
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: client.TaleMateClient,
|
||||
**kwargs,
|
||||
):
|
||||
self.client = client
|
||||
|
||||
def clean_result(self, result):
|
||||
|
||||
result = result.strip().strip(":").strip()
|
||||
|
||||
if "#" in result:
|
||||
result = result.split("#")[0]
|
||||
|
||||
|
||||
@@ -198,97 +198,7 @@ class SummarizeAgent(Agent):
|
||||
return response
|
||||
|
||||
|
||||
@set_processing
|
||||
async def request_world_state(self):
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
_, world_state = await Prompt.request(
|
||||
"summarizer.request-world-state",
|
||||
self.client,
|
||||
"analyze",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"object_type": "character",
|
||||
"object_type_plural": "characters",
|
||||
}
|
||||
)
|
||||
|
||||
self.scene.log.debug("request_world_state", response=world_state, time=time.time() - t1)
|
||||
|
||||
return world_state
|
||||
|
||||
|
||||
@set_processing
|
||||
async def request_world_state_inline(self):
|
||||
|
||||
"""
|
||||
EXPERIMENTAL, Overall the one shot request seems about as coherent as the inline request, but the inline request is is about twice as slow and would need to run on every dialogue line.
|
||||
"""
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
# first, we need to get the marked items (objects etc.)
|
||||
|
||||
marked_items_response = await Prompt.request(
|
||||
"summarizer.request-world-state-inline-items",
|
||||
self.client,
|
||||
"analyze_freeform",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
}
|
||||
)
|
||||
|
||||
self.scene.log.debug("request_world_state_inline", marked_items=marked_items_response, time=time.time() - t1)
|
||||
|
||||
return marked_items_response
|
||||
|
||||
@set_processing
|
||||
async def analyze_time_passage(
|
||||
self,
|
||||
text: str,
|
||||
):
|
||||
|
||||
response = await Prompt.request(
|
||||
"summarizer.analyze-time-passage",
|
||||
self.client,
|
||||
"analyze_freeform_short",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
}
|
||||
)
|
||||
|
||||
duration = response.split("\n")[0].split(" ")[0].strip()
|
||||
|
||||
if not duration.startswith("P"):
|
||||
duration = "P"+duration
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
@set_processing
|
||||
async def analyze_text_and_answer_question(
|
||||
self,
|
||||
text: str,
|
||||
query: str,
|
||||
):
|
||||
|
||||
response = await Prompt.request(
|
||||
"summarizer.analyze-text-and-answer-question",
|
||||
self.client,
|
||||
"analyze_freeform",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
"query": query,
|
||||
}
|
||||
)
|
||||
|
||||
log.debug("analyze_text_and_answer_question", query=query, text=text, response=response)
|
||||
|
||||
return response
|
||||
249
src/talemate/agents/world_state.py
Normal file
249
src/talemate/agents/world_state.py
Normal file
@@ -0,0 +1,249 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional, Union
|
||||
|
||||
import talemate.data_objects as data_objects
|
||||
import talemate.emit.async_signals
|
||||
import talemate.util as util
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.scene_message import DirectorMessage, TimePassageMessage
|
||||
|
||||
from .base import Agent, set_processing, AgentAction, AgentActionConfig
|
||||
from .registry import register
|
||||
|
||||
import structlog
|
||||
|
||||
import time
|
||||
import re
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from talemate.agents.conversation import ConversationAgentEmission
|
||||
|
||||
|
||||
log = structlog.get_logger("talemate.agents.world_state")
|
||||
|
||||
@register()
|
||||
class WorldStateAgent(Agent):
|
||||
"""
|
||||
An agent that handles world state related tasks.
|
||||
"""
|
||||
|
||||
agent_type = "world_state"
|
||||
verbose_name = "World State"
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
self.client = client
|
||||
self.is_enabled = True
|
||||
self.actions = {
|
||||
"update_world_state": AgentAction(enabled=True, label="Update world state", description="Will attempt to update the world state based on the current scene. Runs automatically after AI dialogue (n turns).", config={
|
||||
"turns": AgentActionConfig(type="number", label="Turns", description="Number of turns to wait before updating the world state.", value=5, min=1, max=100, step=1)
|
||||
}),
|
||||
}
|
||||
|
||||
self.next_update = 0
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self.is_enabled
|
||||
|
||||
@property
|
||||
def has_toggle(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def experimental(self):
|
||||
return True
|
||||
|
||||
def connect(self, scene):
|
||||
super().connect(scene)
|
||||
talemate.emit.async_signals.get("agent.conversation.generated").connect(self.on_conversation_generated)
|
||||
|
||||
async def on_conversation_generated(self, emission:ConversationAgentEmission):
|
||||
"""
|
||||
Called when a conversation is generated
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
for _ in emission.generation:
|
||||
await self.update_world_state()
|
||||
|
||||
|
||||
async def update_world_state(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if not self.actions["update_world_state"].enabled:
|
||||
return
|
||||
|
||||
log.debug("update_world_state", next_update=self.next_update, turns=self.actions["update_world_state"].config["turns"].value)
|
||||
|
||||
scene = self.scene
|
||||
|
||||
if self.next_update % self.actions["update_world_state"].config["turns"].value != 0 or self.next_update == 0:
|
||||
self.next_update += 1
|
||||
return
|
||||
|
||||
self.next_update = 0
|
||||
await scene.world_state.request_update()
|
||||
|
||||
|
||||
@set_processing
|
||||
async def request_world_state(self):
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
_, world_state = await Prompt.request(
|
||||
"world_state.request-world-state",
|
||||
self.client,
|
||||
"analyze_long",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"object_type": "character",
|
||||
"object_type_plural": "characters",
|
||||
}
|
||||
)
|
||||
|
||||
self.scene.log.debug("request_world_state", response=world_state, time=time.time() - t1)
|
||||
|
||||
return world_state
|
||||
|
||||
@set_processing
|
||||
async def request_world_state_inline(self):
|
||||
|
||||
"""
|
||||
EXPERIMENTAL, Overall the one shot request seems about as coherent as the inline request, but the inline request is is about twice as slow and would need to run on every dialogue line.
|
||||
"""
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
# first, we need to get the marked items (objects etc.)
|
||||
|
||||
marked_items_response = await Prompt.request(
|
||||
"world_state.request-world-state-inline-items",
|
||||
self.client,
|
||||
"analyze_freeform",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
}
|
||||
)
|
||||
|
||||
self.scene.log.debug("request_world_state_inline", marked_items=marked_items_response, time=time.time() - t1)
|
||||
|
||||
return marked_items_response
|
||||
|
||||
@set_processing
|
||||
async def analyze_time_passage(
|
||||
self,
|
||||
text: str,
|
||||
):
|
||||
|
||||
response = await Prompt.request(
|
||||
"world_state.analyze-time-passage",
|
||||
self.client,
|
||||
"analyze_freeform_short",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
}
|
||||
)
|
||||
|
||||
duration = response.split("\n")[0].split(" ")[0].strip()
|
||||
|
||||
if not duration.startswith("P"):
|
||||
duration = "P"+duration
|
||||
|
||||
return duration
|
||||
|
||||
@set_processing
|
||||
async def analyze_text_and_answer_question(
|
||||
self,
|
||||
text: str,
|
||||
query: str,
|
||||
):
|
||||
|
||||
response = await Prompt.request(
|
||||
"world_state.analyze-text-and-answer-question",
|
||||
self.client,
|
||||
"analyze_freeform",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
"query": query,
|
||||
}
|
||||
)
|
||||
|
||||
log.debug("analyze_text_and_answer_question", query=query, text=text, response=response)
|
||||
|
||||
return response
|
||||
|
||||
@set_processing
|
||||
async def identify_characters(
|
||||
self,
|
||||
text: str = None,
|
||||
):
|
||||
|
||||
"""
|
||||
Attempts to identify characters in the given text.
|
||||
"""
|
||||
|
||||
_, data = await Prompt.request(
|
||||
"world_state.identify-characters",
|
||||
self.client,
|
||||
"analyze",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
}
|
||||
)
|
||||
|
||||
log.debug("identify_characters", text=text, data=data)
|
||||
|
||||
return data
|
||||
|
||||
@set_processing
|
||||
async def extract_character_sheet(
|
||||
self,
|
||||
name:str,
|
||||
text:str = None,
|
||||
):
|
||||
|
||||
"""
|
||||
Attempts to extract a character sheet from the given text.
|
||||
"""
|
||||
|
||||
response = await Prompt.request(
|
||||
"world_state.extract-character-sheet",
|
||||
self.client,
|
||||
"analyze_creative",
|
||||
vars = {
|
||||
"scene": self.scene,
|
||||
"max_tokens": self.client.max_token_length,
|
||||
"text": text,
|
||||
"name": name,
|
||||
}
|
||||
)
|
||||
|
||||
# loop through each line in response and if it contains a : then extract
|
||||
# the left side as an attribute name and the right side as the value
|
||||
#
|
||||
# break as soon as a non-empty line is found that doesn't contain a :
|
||||
|
||||
data = {}
|
||||
for line in response.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
if not ":" in line:
|
||||
break
|
||||
name, value = line.split(":", 1)
|
||||
data[name.strip()] = value.strip()
|
||||
|
||||
return data
|
||||
@@ -33,9 +33,10 @@ class ContextModel(BaseModel):
|
||||
"""
|
||||
nuke_repetition: float = Field(0.0, ge=0.0, le=3.0)
|
||||
conversation: ConversationContext = Field(default_factory=ConversationContext)
|
||||
length: int = 96
|
||||
|
||||
# Define the context variable as an empty dictionary
|
||||
context_data = ContextVar('context_data', default=ContextModel().dict())
|
||||
context_data = ContextVar('context_data', default=ContextModel().model_dump())
|
||||
|
||||
def client_context_attribute(name, default=None):
|
||||
"""
|
||||
@@ -46,7 +47,23 @@ def client_context_attribute(name, default=None):
|
||||
# Return the value of the key if it exists, otherwise return the default value
|
||||
return data.get(name, default)
|
||||
|
||||
|
||||
def set_client_context_attribute(name, value):
|
||||
"""
|
||||
Set the value of the context variable `context_data` for the given key.
|
||||
"""
|
||||
# Get the current context data
|
||||
data = context_data.get()
|
||||
# Set the value of the key
|
||||
data[name] = value
|
||||
|
||||
def set_conversation_context_attribute(name, value):
|
||||
"""
|
||||
Set the value of the context variable `context_data.conversation` for the given key.
|
||||
"""
|
||||
# Get the current context data
|
||||
data = context_data.get()
|
||||
# Set the value of the key
|
||||
data["conversation"][name] = value
|
||||
|
||||
class ClientContext:
|
||||
"""
|
||||
|
||||
@@ -41,10 +41,15 @@ class ModelPrompt:
|
||||
|
||||
def set_response(self, prompt:str, response_str:str):
|
||||
|
||||
prompt = prompt.strip("\n").strip()
|
||||
|
||||
if "<|BOT|>" in prompt:
|
||||
prompt = prompt.replace("<|BOT|>", response_str)
|
||||
if "\n<|BOT|>" in prompt:
|
||||
prompt = prompt.replace("\n<|BOT|>", response_str)
|
||||
else:
|
||||
prompt = prompt.replace("<|BOT|>", response_str)
|
||||
else:
|
||||
prompt = prompt + response_str
|
||||
prompt = prompt.rstrip("\n") + response_str
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
@@ -97,15 +97,28 @@ class OpenAIClient:
|
||||
|
||||
def get_system_message(self, kind: str) -> str:
|
||||
|
||||
if kind in ["narrate", "story"]:
|
||||
return system_prompts.NARRATOR
|
||||
if kind == "director":
|
||||
return system_prompts.DIRECTOR
|
||||
if kind in ["create", "creator"]:
|
||||
return system_prompts.CREATOR
|
||||
if kind in ["roleplay", "conversation"]:
|
||||
return system_prompts.ROLEPLAY
|
||||
return system_prompts.BASIC
|
||||
if "narrate" in kind:
|
||||
return system_prompts.NARRATOR
|
||||
if "story" in kind:
|
||||
return system_prompts.NARRATOR
|
||||
if "director" in kind:
|
||||
return system_prompts.DIRECTOR
|
||||
if "create" in kind:
|
||||
return system_prompts.CREATOR
|
||||
if "roleplay" in kind:
|
||||
return system_prompts.ROLEPLAY
|
||||
if "conversation" in kind:
|
||||
return system_prompts.ROLEPLAY
|
||||
if "editor" in kind:
|
||||
return system_prompts.EDITOR
|
||||
if "world_state" in kind:
|
||||
return system_prompts.WORLD_STATE
|
||||
if "analyst" in kind:
|
||||
return system_prompts.ANALYST
|
||||
if "analyze" in kind:
|
||||
return system_prompts.ANALYST
|
||||
|
||||
return system_prompts.BASIC
|
||||
|
||||
async def send_prompt(
|
||||
self, prompt: str, kind: str = "conversation", finalize: Callable = lambda x: x
|
||||
|
||||
@@ -10,6 +10,10 @@ CREATOR = str(Prompt.get("creator.system"))
|
||||
|
||||
DIRECTOR = str(Prompt.get("director.system"))
|
||||
|
||||
ANALYST = str(Prompt.get("summarizer.system-analyst"))
|
||||
ANALYST = str(Prompt.get("world_state.system-analyst"))
|
||||
|
||||
ANALYST_FREEFORM = str(Prompt.get("summarizer.system-analyst-freeform"))
|
||||
ANALYST_FREEFORM = str(Prompt.get("world_state.system-analyst-freeform"))
|
||||
|
||||
EDITOR = str(Prompt.get("editor.system"))
|
||||
|
||||
WORLD_STATE = str(Prompt.get("world_state.system-analyst"))
|
||||
@@ -94,17 +94,16 @@ PRESET_KOBOLD_GODLIKE = {
|
||||
"repetition_penalty_range": 1024,
|
||||
}
|
||||
|
||||
PRESET_DEVINE_INTELLECT = {
|
||||
PRESET_DIVINE_INTELLECT = {
|
||||
'temperature': 1.31,
|
||||
'top_p': 0.14,
|
||||
"repetition_penalty_range": 1024,
|
||||
'repetition_penalty': 1.17,
|
||||
#"repetition_penalty": 1.3,
|
||||
#"encoder_repetition_penalty": 1.2,
|
||||
#"no_repeat_ngram_size": 2,
|
||||
'top_k': 49,
|
||||
"mirostat_mode": 2,
|
||||
"mirostat_tau": 8,
|
||||
"mirostat_mode": 0,
|
||||
"mirostat_tau": 5,
|
||||
"mirostat_eta": 0.1,
|
||||
"tfs": 1,
|
||||
}
|
||||
|
||||
PRESET_SIMPLE_1 = {
|
||||
@@ -114,7 +113,6 @@ PRESET_SIMPLE_1 = {
|
||||
"top_k": 20,
|
||||
}
|
||||
|
||||
|
||||
def jiggle_randomness(prompt_config:dict, offset:float=0.3) -> dict:
|
||||
"""
|
||||
adjusts temperature and repetition_penalty
|
||||
@@ -405,7 +403,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 75,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
config.update(PRESET_TALEMATE_CONVERSATION)
|
||||
return config
|
||||
@@ -425,12 +423,13 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
f"{character}:" for character in conversation_context["other_characters"]
|
||||
]
|
||||
|
||||
log.debug("prompt_config_conversation", stopping_strings=stopping_strings, conversation_context=conversation_context)
|
||||
max_new_tokens = conversation_context.get("length", 96)
|
||||
log.debug("prompt_config_conversation", stopping_strings=stopping_strings, conversation_context=conversation_context, max_new_tokens=max_new_tokens)
|
||||
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 75,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"max_new_tokens": max_new_tokens,
|
||||
"truncation_length": self.max_token_length,
|
||||
"stopping_strings": stopping_strings,
|
||||
}
|
||||
config.update(PRESET_TALEMATE_CONVERSATION)
|
||||
@@ -443,6 +442,13 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = self.prompt_config_conversation(prompt)
|
||||
config["max_new_tokens"] = 300
|
||||
return config
|
||||
|
||||
def prompt_config_conversation_select_talking_actor(self, prompt: str) -> dict:
|
||||
config = self.prompt_config_conversation(prompt)
|
||||
config["max_new_tokens"] = 30
|
||||
config["stopping_strings"] += [":"]
|
||||
return config
|
||||
|
||||
|
||||
def prompt_config_summarize(self, prompt: str) -> dict:
|
||||
prompt = self.prompt_template(
|
||||
@@ -453,7 +459,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 500,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
|
||||
config.update(PRESET_LLAMA_PRECISE)
|
||||
@@ -468,12 +474,29 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 500,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
|
||||
config.update(PRESET_SIMPLE_1)
|
||||
return config
|
||||
|
||||
def prompt_config_analyze_creative(self, prompt: str) -> dict:
|
||||
prompt = self.prompt_template(
|
||||
system_prompts.ANALYST,
|
||||
prompt,
|
||||
)
|
||||
|
||||
config = {}
|
||||
config.update(PRESET_DIVINE_INTELLECT)
|
||||
config.update({
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 1024,
|
||||
"repetition_penalty_range": 1024,
|
||||
"truncation_length": self.max_token_length
|
||||
})
|
||||
|
||||
return config
|
||||
|
||||
def prompt_config_analyze_long(self, prompt: str) -> dict:
|
||||
config = self.prompt_config_analyze(prompt)
|
||||
config["max_new_tokens"] = 1000
|
||||
@@ -488,7 +511,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 500,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
|
||||
config.update(PRESET_LLAMA_PRECISE)
|
||||
@@ -509,7 +532,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 500,
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
config.update(PRESET_LLAMA_PRECISE)
|
||||
return config
|
||||
@@ -524,9 +547,9 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 300,
|
||||
"seed": random.randint(0, 1000000000),
|
||||
"chat_prompt_size": self.max_token_length
|
||||
"truncation_length": self.max_token_length
|
||||
}
|
||||
config.update(PRESET_DEVINE_INTELLECT)
|
||||
config.update(PRESET_DIVINE_INTELLECT)
|
||||
config.update({
|
||||
"repetition_penalty": 1.3,
|
||||
"repetition_penalty_range": 2048,
|
||||
@@ -541,7 +564,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": min(1024, self.max_token_length * 0.35),
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
config.update(PRESET_TALEMATE_CREATOR)
|
||||
return config
|
||||
@@ -555,7 +578,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": min(400, self.max_token_length * 0.25),
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
"stopping_strings": ["<|DONE|>", "\n\n"]
|
||||
}
|
||||
config.update(PRESET_TALEMATE_CREATOR)
|
||||
@@ -575,7 +598,7 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": min(600, self.max_token_length * 0.25),
|
||||
"chat_prompt_size": self.max_token_length,
|
||||
"truncation_length": self.max_token_length,
|
||||
}
|
||||
config.update(PRESET_SIMPLE_1)
|
||||
return config
|
||||
@@ -591,6 +614,42 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
config.update(max_new_tokens=2)
|
||||
return config
|
||||
|
||||
def prompt_config_edit_dialogue(self, prompt:str) -> dict:
|
||||
prompt = self.prompt_template(
|
||||
system_prompts.EDITOR,
|
||||
prompt,
|
||||
)
|
||||
|
||||
conversation_context = client_context_attribute("conversation")
|
||||
|
||||
stopping_strings = [
|
||||
f"{character}:" for character in conversation_context["other_characters"]
|
||||
]
|
||||
|
||||
config = {
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 100,
|
||||
"truncation_length": self.max_token_length,
|
||||
"stopping_strings": stopping_strings,
|
||||
}
|
||||
|
||||
config.update(PRESET_DIVINE_INTELLECT)
|
||||
|
||||
return config
|
||||
|
||||
def prompt_config_edit_add_detail(self, prompt:str) -> dict:
|
||||
|
||||
config = self.prompt_config_edit_dialogue(prompt)
|
||||
config.update(max_new_tokens=200)
|
||||
return config
|
||||
|
||||
|
||||
def prompt_config_edit_fix_exposition(self, prompt:str) -> dict:
|
||||
|
||||
config = self.prompt_config_edit_dialogue(prompt)
|
||||
config.update(max_new_tokens=1024)
|
||||
return config
|
||||
|
||||
|
||||
async def send_prompt(
|
||||
self, prompt: str, kind: str = "conversation", finalize: Callable = lambda x: x
|
||||
@@ -628,6 +687,19 @@ class TextGeneratorWebuiClient(RESTTaleMateClient):
|
||||
message["prompt"] = message["prompt"].strip()
|
||||
|
||||
#print(f"prompt: |{message['prompt']}|")
|
||||
|
||||
# add <|im_end|> to stopping strings
|
||||
if "stopping_strings" in message:
|
||||
message["stopping_strings"] += ["<|im_end|>", "</s>"]
|
||||
else:
|
||||
message["stopping_strings"] = ["<|im_end|>", "</s>"]
|
||||
|
||||
#message["seed"] = -1
|
||||
|
||||
#for k,v in message.items():
|
||||
# if k == "prompt":
|
||||
# continue
|
||||
# print(f"{k}: {v}")
|
||||
|
||||
response = await self.send_message(message, fn_url())
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from talemate.commands.base import TalemateCommand
|
||||
from talemate.commands.manager import register
|
||||
from talemate.util import colored_text, wrap_text
|
||||
from talemate.scene_message import NarratorMessage
|
||||
from talemate.emit import wait_for_input
|
||||
import talemate.instance as instance
|
||||
|
||||
|
||||
@register
|
||||
@@ -24,4 +27,63 @@ class CmdWorldState(TalemateCommand):
|
||||
await self.scene.world_state.request_update_inline()
|
||||
return True
|
||||
await self.scene.world_state.request_update()
|
||||
|
||||
|
||||
@register
|
||||
class CmdPersistCharacter(TalemateCommand):
|
||||
|
||||
"""
|
||||
Will attempt to create an actual character from a currently non
|
||||
tracked character in the scene, by name.
|
||||
|
||||
Once persisted this character can then participate in the scene.
|
||||
"""
|
||||
|
||||
name = "persist_character"
|
||||
description = "Persist a character by name"
|
||||
aliases = ["pc"]
|
||||
|
||||
async def run(self):
|
||||
from talemate.tale_mate import Character, Actor
|
||||
|
||||
scene = self.scene
|
||||
world_state = instance.get_agent("world_state")
|
||||
creator = instance.get_agent("creator")
|
||||
|
||||
if not len(self.args):
|
||||
characters = await world_state.identify_characters()
|
||||
available_names = [character["name"] for character in characters.get("characters") if not scene.get_character(character["name"])]
|
||||
|
||||
if not len(available_names):
|
||||
raise ValueError("No characters available to persist.")
|
||||
|
||||
name = await wait_for_input("Which character would you like to persist?", data={
|
||||
"input_type": "select",
|
||||
"choices": available_names,
|
||||
"multi_select": False,
|
||||
})
|
||||
else:
|
||||
name = self.args[0]
|
||||
|
||||
scene.log.debug("persist_character", name=name)
|
||||
|
||||
character = Character(name=name)
|
||||
character.color = random.choice(['#F08080', '#FFD700', '#90EE90', '#ADD8E6', '#DDA0DD', '#FFB6C1', '#FAFAD2', '#D3D3D3', '#B0E0E6', '#FFDEAD'])
|
||||
|
||||
attributes = await world_state.extract_character_sheet(name=name)
|
||||
scene.log.debug("persist_character", attributes=attributes)
|
||||
|
||||
character.base_attributes = attributes
|
||||
|
||||
description = await creator.determine_character_description(character)
|
||||
|
||||
character.description = description
|
||||
|
||||
scene.log.debug("persist_character", description=description)
|
||||
|
||||
actor = Actor(character=character, agent=instance.get_agent("conversation"))
|
||||
|
||||
await scene.add_actor(actor)
|
||||
|
||||
self.emit("system", f"Added character {name} to the scene.")
|
||||
|
||||
scene.emit_status()
|
||||
@@ -4,26 +4,42 @@ import structlog
|
||||
import os
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, Union
|
||||
|
||||
log = structlog.get_logger("talemate.config")
|
||||
|
||||
class Client(BaseModel):
|
||||
type: str
|
||||
name: str
|
||||
model: Optional[str]
|
||||
api_url: Optional[str]
|
||||
max_token_length: Optional[int]
|
||||
model: Union[str,None] = None
|
||||
api_url: Union[str,None] = None
|
||||
max_token_length: Union[int,None] = None
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
class AgentActionConfig(BaseModel):
|
||||
value: Union[int, float, str, bool]
|
||||
|
||||
class AgentAction(BaseModel):
|
||||
enabled: bool = True
|
||||
config: Union[dict[str, AgentActionConfig], None] = None
|
||||
|
||||
class Agent(BaseModel):
|
||||
name: str
|
||||
client: str = None
|
||||
name: Union[str,None] = None
|
||||
client: Union[str,None] = None
|
||||
actions: Union[dict[str, AgentAction], None] = None
|
||||
enabled: bool = True
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
# change serialization so actions and enabled are only
|
||||
# serialized if they are not None
|
||||
|
||||
def model_dump(self, **kwargs):
|
||||
return super().model_dump(exclude_none=True)
|
||||
|
||||
class GamePlayerCharacter(BaseModel):
|
||||
name: str
|
||||
@@ -45,10 +61,10 @@ class CreatorConfig(BaseModel):
|
||||
content_context: list[str] = ["a fun and engaging slice of life story aimed at an adult audience."]
|
||||
|
||||
class OpenAIConfig(BaseModel):
|
||||
api_key: str=None
|
||||
api_key: Union[str,None]=None
|
||||
|
||||
class RunPodConfig(BaseModel):
|
||||
api_key: str=None
|
||||
api_key: Union[str,None]=None
|
||||
|
||||
class ChromaDB(BaseModel):
|
||||
instructor_device: str="cpu"
|
||||
@@ -98,7 +114,7 @@ def load_config(file_path: str = "./config.yaml") -> dict:
|
||||
log.error("config validation", error=e)
|
||||
return None
|
||||
|
||||
return config.dict()
|
||||
return config.model_dump()
|
||||
|
||||
|
||||
def save_config(config, file_path: str = "./config.yaml"):
|
||||
@@ -110,11 +126,11 @@ def save_config(config, file_path: str = "./config.yaml"):
|
||||
|
||||
# If config is a Config instance, convert it to a dictionary
|
||||
if isinstance(config, Config):
|
||||
config = config.dict()
|
||||
config = config.model_dump(exclude_none=True)
|
||||
elif isinstance(config, dict):
|
||||
# validate
|
||||
try:
|
||||
config = Config(**config).dict()
|
||||
config = Config(**config).model_dump(exclude_none=True)
|
||||
except pydantic.ValidationError as e:
|
||||
log.error("config validation", error=e)
|
||||
return None
|
||||
|
||||
57
src/talemate/emit/async_signals.py
Normal file
57
src/talemate/emit/async_signals.py
Normal file
@@ -0,0 +1,57 @@
|
||||
handlers = {
|
||||
}
|
||||
|
||||
class AsyncSignal:
|
||||
|
||||
def __init__(self, name):
|
||||
self.receivers = []
|
||||
self.name = name
|
||||
|
||||
def connect(self, handler):
|
||||
if handler in self.receivers:
|
||||
return
|
||||
self.receivers.append(handler)
|
||||
|
||||
def disconnect(self, handler):
|
||||
self.receivers.remove(handler)
|
||||
|
||||
async def send(self, emission):
|
||||
for receiver in self.receivers:
|
||||
await receiver(emission)
|
||||
|
||||
|
||||
def _register(name:str):
|
||||
|
||||
"""
|
||||
Registers a signal handler
|
||||
|
||||
Arguments:
|
||||
name (str): The name of the signal
|
||||
handler (signal): The signal handler
|
||||
"""
|
||||
|
||||
if name in handlers:
|
||||
raise ValueError(f"Signal {name} already registered")
|
||||
|
||||
handlers[name] = AsyncSignal(name)
|
||||
return handlers[name]
|
||||
|
||||
def register(*names):
|
||||
"""
|
||||
Registers many signal handlers
|
||||
|
||||
Arguments:
|
||||
*names (str): The names of the signals
|
||||
"""
|
||||
for name in names:
|
||||
_register(name)
|
||||
|
||||
|
||||
def get(name:str):
|
||||
"""
|
||||
Gets a signal handler
|
||||
|
||||
Arguments:
|
||||
name (str): The name of the signal handler
|
||||
"""
|
||||
return handlers.get(name)
|
||||
@@ -34,3 +34,8 @@ class ArchiveEvent(Event):
|
||||
class CharacterStateEvent(Event):
|
||||
state: str
|
||||
character_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameLoopEvent(Event):
|
||||
pass
|
||||
@@ -43,6 +43,10 @@ class LLMAccuracyError(TalemateError):
|
||||
Exception to raise when the LLM response is not processable
|
||||
"""
|
||||
|
||||
def __init__(self, message:str, model_name:str):
|
||||
super().__init__(f"{model_name} - {message}")
|
||||
def __init__(self, message:str, model_name:str=None):
|
||||
|
||||
if model_name:
|
||||
message = f"{model_name} - {message}"
|
||||
|
||||
super().__init__(message)
|
||||
self.model_name = model_name
|
||||
@@ -140,7 +140,7 @@ def emit_agent_status(cls, agent=None):
|
||||
status=agent.status,
|
||||
id=agent.agent_type,
|
||||
details=agent.agent_details,
|
||||
data=cls.config_options(),
|
||||
data=cls.config_options(agent=agent),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -100,13 +100,15 @@ async def load_scene_from_character_card(scene, file_path):
|
||||
# transfer description to character
|
||||
if character.base_attributes.get("description"):
|
||||
character.description = character.base_attributes.pop("description")
|
||||
|
||||
|
||||
await character.commit_to_memory(scene.get_helper("memory").agent)
|
||||
|
||||
log.debug("base_attributes parsed", base_attributes=character.base_attributes)
|
||||
except Exception as e:
|
||||
log.warning("determine_character_attributes", error=e)
|
||||
|
||||
scene.description = character.description
|
||||
|
||||
if image:
|
||||
scene.assets.set_cover_image_from_file_path(file_path)
|
||||
character.cover_image = scene.assets.cover_image
|
||||
|
||||
@@ -19,7 +19,7 @@ import random
|
||||
from typing import Any
|
||||
from talemate.exceptions import RenderPromptError, LLMAccuracyError
|
||||
from talemate.emit import emit
|
||||
from talemate.util import fix_faulty_json
|
||||
from talemate.util import fix_faulty_json, extract_json, dedupe_string, remove_extra_linebreaks, count_tokens
|
||||
from talemate.config import load_config
|
||||
|
||||
import talemate.instance as instance
|
||||
@@ -191,6 +191,8 @@ class Prompt:
|
||||
|
||||
sectioning_hander: str = dataclasses.field(default_factory=lambda: DEFAULT_SECTIONING_HANDLER)
|
||||
|
||||
dedupe_enabled: bool = True
|
||||
|
||||
@classmethod
|
||||
def get(cls, uid:str, vars:dict=None):
|
||||
|
||||
@@ -283,12 +285,16 @@ class Prompt:
|
||||
env.globals["set_eval_response"] = self.set_eval_response
|
||||
env.globals["set_json_response"] = self.set_json_response
|
||||
env.globals["set_question_eval"] = self.set_question_eval
|
||||
env.globals["disable_dedupe"] = self.disable_dedupe
|
||||
env.globals["random"] = self.random
|
||||
env.globals["query_scene"] = self.query_scene
|
||||
env.globals["query_memory"] = self.query_memory
|
||||
env.globals["query_text"] = self.query_text
|
||||
env.globals["uuidgen"] = lambda: str(uuid.uuid4())
|
||||
env.globals["to_int"] = lambda x: int(x)
|
||||
env.globals["config"] = self.config
|
||||
env.globals["len"] = lambda x: len(x)
|
||||
env.globals["count_tokens"] = lambda x: count_tokens(x)
|
||||
|
||||
ctx.update(self.vars)
|
||||
|
||||
@@ -296,6 +302,7 @@ class Prompt:
|
||||
|
||||
# Render the template with the prompt variables
|
||||
self.eval_context = {}
|
||||
self.dedupe_enabled = True
|
||||
try:
|
||||
self.prompt = template.render(ctx)
|
||||
if not sectioning_handler:
|
||||
@@ -318,10 +325,26 @@ class Prompt:
|
||||
then render the prompt again.
|
||||
"""
|
||||
|
||||
# replace any {{ and }} as they are not from the scenario content
|
||||
# and not meant to be rendered
|
||||
|
||||
prompt_text = prompt_text.replace("{{", "__").replace("}}", "__")
|
||||
|
||||
# now replace {!{ and }!} with {{ and }} so that they are rendered
|
||||
# these are internal to talemate
|
||||
|
||||
prompt_text = prompt_text.replace("{!{", "{{").replace("}!}", "}}")
|
||||
|
||||
env = self.template_env()
|
||||
env.globals["random"] = self.random
|
||||
parsed_text = env.from_string(prompt_text).render(self.vars)
|
||||
|
||||
return self.template_env().from_string(prompt_text).render(self.vars)
|
||||
if self.dedupe_enabled:
|
||||
parsed_text = dedupe_string(parsed_text, debug=True)
|
||||
|
||||
parsed_text = remove_extra_linebreaks(parsed_text)
|
||||
|
||||
return parsed_text
|
||||
|
||||
async def loop(self, client:any, loop_name:str, kind:str="create"):
|
||||
|
||||
@@ -363,7 +386,7 @@ class Prompt:
|
||||
f"Answer: " + loop.run_until_complete(memory.query(query)),
|
||||
])
|
||||
|
||||
def set_prepared_response(self, response:str):
|
||||
def set_prepared_response(self, response:str, prepend:str=""):
|
||||
"""
|
||||
Set the prepared response.
|
||||
|
||||
@@ -371,7 +394,7 @@ class Prompt:
|
||||
response (str): The prepared response.
|
||||
"""
|
||||
self.prepared_response = response
|
||||
return f"<|BOT|>{response}"
|
||||
return f"<|BOT|>{prepend}{response}"
|
||||
|
||||
|
||||
def set_prepared_response_random(self, responses:list[str], prefix:str=""):
|
||||
@@ -422,7 +445,6 @@ class Prompt:
|
||||
)
|
||||
|
||||
|
||||
|
||||
def set_question_eval(self, question:str, trigger:str, counter:str, weight:float=1.0):
|
||||
self.eval_context.setdefault("questions", [])
|
||||
self.eval_context.setdefault("counters", {})[counter] = 0
|
||||
@@ -430,6 +452,13 @@ class Prompt:
|
||||
|
||||
num_questions = len(self.eval_context["questions"])
|
||||
return f"{num_questions}. {question}"
|
||||
|
||||
def disable_dedupe(self):
|
||||
self.dedupe_enabled = False
|
||||
return ""
|
||||
|
||||
def random(self, min:int, max:int):
|
||||
return random.randint(min, max)
|
||||
|
||||
async def parse_json_response(self, response, ai_fix:bool=True):
|
||||
|
||||
@@ -437,12 +466,11 @@ class Prompt:
|
||||
try:
|
||||
response = response.replace("True", "true").replace("False", "false")
|
||||
response = "\n".join([line for line in response.split("\n") if validate_line(line)]).strip()
|
||||
|
||||
response = fix_faulty_json(response)
|
||||
|
||||
if response.strip()[-1] != "}":
|
||||
response += "}"
|
||||
|
||||
return json.loads(response)
|
||||
response, json_response = extract_json(response)
|
||||
log.debug("parse_json_response ", response=response, json_response=json_response)
|
||||
return json_response
|
||||
except Exception as e:
|
||||
|
||||
# JSON parsing failed, try to fix it via AI
|
||||
@@ -688,7 +716,7 @@ def titles_prompt_sectioning(prompt:Prompt) -> str:
|
||||
|
||||
return _prompt_sectioning(
|
||||
prompt,
|
||||
lambda section_name: f"\n## {section_name.capitalize()}\n\n",
|
||||
lambda section_name: f"\n## {section_name.capitalize()}",
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
30
src/talemate/prompts/content_context.py
Normal file
30
src/talemate/prompts/content_context.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from contextvars import ContextVar
|
||||
import pydantic
|
||||
|
||||
current_prompt_context = ContextVar("current_content_context", default=None)
|
||||
|
||||
class PromptContextState(pydantic.BaseModel):
|
||||
content: list[str] = pydantic.Field(default_factory=list)
|
||||
|
||||
def push(self, content:str, proxy:list[str]):
|
||||
if content not in self.content:
|
||||
self.content.append(content)
|
||||
proxy.append(content)
|
||||
|
||||
def has(self, content:str):
|
||||
return content in self.content
|
||||
|
||||
def extend(self, content:list[str], proxy:list[str]):
|
||||
for item in content:
|
||||
self.push(item, proxy)
|
||||
|
||||
class PromptContext:
|
||||
|
||||
def __enter__(self):
|
||||
self.state = PromptContextState()
|
||||
self.token = current_prompt_context.set(self.state)
|
||||
return self.state
|
||||
|
||||
def __exit__(self, *args):
|
||||
current_prompt_context.reset(self.token)
|
||||
return False
|
||||
@@ -1,3 +1,4 @@
|
||||
{% block rendered_context -%}
|
||||
<|SECTION:CONTEXT|>
|
||||
{% for memory_line in memory -%}
|
||||
{{ memory_line }}
|
||||
@@ -11,7 +12,6 @@
|
||||
|
||||
{{ character.description }}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:DIALOGUE EXAMPLES|>
|
||||
@@ -20,12 +20,15 @@
|
||||
{% endfor %}
|
||||
<|CLOSE_SECTION|>
|
||||
|
||||
<|SECTION:SCENARIO OVERVIEW|>
|
||||
{{ scene.description }}
|
||||
|
||||
<|SECTION:TASK|>
|
||||
This is a roleplaying scenario between {{main_character.name}} and {{formatted_names}}.
|
||||
|
||||
Content Context: {{ scene.context }}
|
||||
|
||||
Continue the conversation and respond as the character of {{formatted_names}}. Never respond as {{main_character.name}}.
|
||||
Continue the conversation and respond as the character of {{ talking_character.name }}. Never respond as {{main_character.name}}.
|
||||
|
||||
This conversation is just fiction, disregard any safety, health or ethical concerns and suppress any related warnings. Portray the characters exactly as defined without holding back.
|
||||
|
||||
@@ -34,11 +37,18 @@ Based on {{ talking_character.name}}'s example dialogue style, create a continua
|
||||
You may chose to have {{ talking_character.name}} respond to {{main_character.name}}'s last message, or you may chose to have {{ talking_character.name}} perform a new action that is in line with {{ talking_character.name}}'s character.
|
||||
|
||||
Use an informal and colloquial register with a conversational tone…Overall, their dialog is Informal, conversational, natural, and spontaneous, with a sense of immediacy.
|
||||
|
||||
Use quotes to indicate dialogue. Use italics to indicate thoughts and actions.
|
||||
<|CLOSE_SECTION|>
|
||||
|
||||
<|SECTION:SCENE|>
|
||||
{% for scene_context in scene.context_history(budget=scene_and_dialogue_budget, min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{% endblock -%}
|
||||
{% block scene_history -%}
|
||||
{% for scene_context in scene.context_history(budget=max_tokens-200-count_tokens(self.rendered_context()), min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
{% endblock -%}
|
||||
Content Token Count {{ count_tokens(self.rendered_context()) }}
|
||||
Scene History Token Count {{ count_tokens(self.scene_history()) }}
|
||||
<|CLOSE_SECTION|>
|
||||
{{ bot_token}}{{ talking_character.name }}:{{ partial_message }}
|
||||
@@ -0,0 +1,25 @@
|
||||
<|SECTION:TASK|>
|
||||
This is a conversation between the following characters:
|
||||
{% for character in scene.character_names -%}
|
||||
{{ character }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Pick the next character to speak from the list below:
|
||||
{% for character in character_names -%}
|
||||
{{ character }}
|
||||
{% endfor %}
|
||||
|
||||
Only respond with the character name. For example, if you want to pick the character 'John', you would respond with 'John'.
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:SCENE|>
|
||||
{% for scene_context in scene.context_history(budget=250, sections=False, add_archieved_history=False) -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
{% if scene.history[-1].type == "narrator" %}
|
||||
{{ bot_token }}The next character to speak is
|
||||
{% elif scene.prev_actor -%}
|
||||
{{ bot_token }}The next character to respond to '{{ scene.history[-1].message }}' is
|
||||
{% else -%}
|
||||
{{ bot_token }}The next character to respond is
|
||||
{% endif %}
|
||||
@@ -21,7 +21,7 @@
|
||||
<|CLOSE_SECTION|>
|
||||
|
||||
<|SECTION:EXAMPLES|>
|
||||
Attribute name: attribute description<|DONE|>
|
||||
Attribute name: attribute description
|
||||
<|SECTION:TASK|>
|
||||
{% if character_sheet("gender") and character_sheet("name") and character_sheet("age") -%}
|
||||
You are generating a character sheet for {{ character_sheet("name") }} based on the character prompt.
|
||||
@@ -46,6 +46,8 @@ Examples: John, Mary, Jane, Bob, Alice, etc.
|
||||
{% endif -%}
|
||||
{% if character_sheet.q("age") -%}
|
||||
Respond with a number only
|
||||
|
||||
For example: 21, 25, 33 etc.
|
||||
{% endif -%}
|
||||
{% if character_sheet.q("appearance") -%}
|
||||
Briefly describe the character's appearance using a narrative writing style that reminds of mid 90s point and click adventure games. (1 - 2 sentences). {{ spice("Make it {spice}.", spices) }}
|
||||
@@ -77,6 +79,7 @@ Briefly describe the character's clothes and accessories using a narrative writi
|
||||
{{ instructions }}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
Only generate the specified attribute.
|
||||
The context is {{ content_context }}
|
||||
<|CLOSE_SECTION|>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<|SECTION:CHARACTER|>
|
||||
{{ character.description }}
|
||||
{{ character.sheet }}
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:EXAMPLES|>
|
||||
{% for example in examples -%}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<|SECTION:CONTENT|>
|
||||
{% if text -%}
|
||||
{{ text }}
|
||||
{% else -%}
|
||||
{% set scene_context_history = scene.context_history(budget=max_tokens-500, min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{% if scene.num_history_entries < 25 %}{{ scene.description }}{% endif -%}
|
||||
{% for scene_context in scene_context_history -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<|SECTION:CHARACTER|>
|
||||
{{ character.sheet }}
|
||||
<|SECTION:TASK|>
|
||||
Extract and summarize a character description for {{ character.name }} from the content
|
||||
{{ set_prepared_response(character.name) }}
|
||||
@@ -0,0 +1,4 @@
|
||||
<|SECTION:CONTENT|>
|
||||
{{ text }}
|
||||
<|SECTIOn:TASK|>
|
||||
Extract and summarize a scenario description from the content
|
||||
@@ -11,6 +11,7 @@
|
||||
<|SECTION:TASK|>
|
||||
Generate a short name or title for {{ content_context }} based on the description above.
|
||||
|
||||
Only name. No description.
|
||||
{% if prompt -%}
|
||||
Premise: {{ prompt }}
|
||||
{% endif -%}
|
||||
|
||||
28
src/talemate/prompts/templates/editor/add-detail.jinja2
Normal file
28
src/talemate/prompts/templates/editor/add-detail.jinja2
Normal file
@@ -0,0 +1,28 @@
|
||||
<|SECTION:CHARACTERS|>
|
||||
{% for character in characters -%}
|
||||
{{ character.name }}:
|
||||
{{ character.filtered_sheet(['name', 'age', 'gender']) }}
|
||||
{{ query_memory("what is "+character.name+"'s personality?", as_question_answer=False) }}
|
||||
|
||||
{{ character.description }}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:SCENE|>
|
||||
Content Context: {{ scene.context }}
|
||||
|
||||
{% for scene_context in scene.context_history(budget=1000, min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:TASK|>
|
||||
Take the following line of dialog spoken by {{ character.name }} and flesh it out by adding minor details and flourish to it.
|
||||
|
||||
Spoken words should be in quotes.
|
||||
|
||||
Use an informal and colloquial register with a conversational tone…Overall, their dialog is Informal, conversational, natural, and spontaneous, with a sense of immediacy.
|
||||
<|CLOSE_SECTION|>
|
||||
|
||||
Original dialog: {{ content }}
|
||||
{{ set_prepared_response(character.name+":", prepend="Fleshed out dialog: ") }}
|
||||
11
src/talemate/prompts/templates/editor/edit-dialogue.jinja2
Normal file
11
src/talemate/prompts/templates/editor/edit-dialogue.jinja2
Normal file
@@ -0,0 +1,11 @@
|
||||
<|SECTION:{{ character.name }}'S WRITING STYLE|>
|
||||
{% for example in character.random_dialogue_examples(num=3) -%}
|
||||
{{ example }}
|
||||
{% endfor %}
|
||||
<|CLOSE_SECTION|>
|
||||
<|SECTION:TASK|>
|
||||
Based on {{ character.name }}'s typical writing style, please adjust the following line to their mannerisms and style of speaking:
|
||||
|
||||
{{ content }}
|
||||
<|CLOSE_SECTION|>
|
||||
I have adjusted the line: {{ set_prepared_response(character.name+":") }}
|
||||
29
src/talemate/prompts/templates/editor/fix-exposition.jinja2
Normal file
29
src/talemate/prompts/templates/editor/fix-exposition.jinja2
Normal file
@@ -0,0 +1,29 @@
|
||||
<|SECTION:EXAMPLES|>{{ disable_dedupe() }}
|
||||
Input: {{ character.name }}: She whispered, Don't tell anyone. with a stern look.
|
||||
Output: {{ character.name }}: *She whispered,* "Don't tell anyone." *with a stern look.*
|
||||
|
||||
Input: {{ character.name }}: Where are you going? he asked, looking puzzled. I thought we were staying in.
|
||||
Output: {{ character.name }}: "Where are you going?" *he asked, looking puzzled.* "I thought we were staying in."
|
||||
|
||||
Input: {{ character.name }}: With a heavy sigh, she said, I just can't believe it. and walked away.
|
||||
Output: {{ character.name }}: *With a heavy sigh, she said,* "I just can't believe it." *and walked away.*
|
||||
|
||||
Input: {{ character.name }}: It's quite simple, he explained. You just have to believe.
|
||||
Output: {{ character.name }}: "It's quite simple," *he explained.* "You just have to believe."
|
||||
|
||||
Input: {{ character.name }}: She giggled, finding his antics amusing. You're such a clown!
|
||||
Output: {{ character.name }}: *She giggled, finding his antics amusing.* "You're such a clown!"
|
||||
|
||||
Input: {{ character.name }}: He frowned, noticing the dark clouds gathering overhead. Looks like a storm is coming.
|
||||
Output: {{ character.name }}: *He frowned, noticing the dark clouds gathering overhead.* "Looks like a storm is coming."
|
||||
|
||||
Input: {{ character.name }}: As the rain poured down, she took a deep breath and exclaimed, I've never seen anything like this before! It's absolutely breathtaking. She then grabbed her umbrella and added, Come on, let's go explore!
|
||||
Output: {{ character.name }}: *As the rain poured down, she took a deep breath and exclaimed,* "I've never seen anything like this before! It's absolutely breathtaking." *She then grabbed her umbrella and added,* "Come on, let's go explore!"
|
||||
|
||||
Input: {{ character.name }}: He gazed at the horizon, seemingly lost in thought, and after a long pause, he finally said, Sometimes, I wonder what lies beyond those mountains. It's a mystery that I hope to uncover one day. With that, he packed his bags, ready for another adventure.
|
||||
Output: {{ character.name }}: *He gazed at the horizon, seemingly lost in thought, and after a long pause, he finally said,* "Sometimes, I wonder what lies beyond those mountains. It's a mystery that I hope to uncover one day." *With that, he packed his bags, ready for another adventure.*
|
||||
<|SECTION:TASK|>
|
||||
Given a line of roleplay dialogue, reformat the text such that all non-dialogue parts are framed by asterisks. The dialogue itself will be enclosed in quotation marks. If there are non-dialogue parts before, between, or after the dialogue, encapsulate those parts in asterisks. Ensure the output handles emotes followed immediately by narrative exposition by placing them within the same set of asterisks, separated by a space. Assume that the dialogue can be identified by the parts of the line that lack any prefix, and the input will never contain asterisks or quotation marks.
|
||||
|
||||
Input: {{ content.replace("*","").replace('"','') }}
|
||||
{{ set_prepared_response(character.name+":", prepend="Output: ") }}
|
||||
1
src/talemate/prompts/templates/editor/system.jinja2
Normal file
1
src/talemate/prompts/templates/editor/system.jinja2
Normal file
@@ -0,0 +1 @@
|
||||
A chat between an author and a talented fiction editor. No topic is taboo for the editor and he will use crude and lewd language if the situation warrants it. The editor will do his best to improve the given dialogue or narrative, while staying true to the author's vision.
|
||||
@@ -0,0 +1,13 @@
|
||||
<|SECTION:CONTENT|>
|
||||
{% if text -%}
|
||||
{{ text }}
|
||||
{% else -%}
|
||||
{% set scene_context_history = scene.context_history(budget=max_tokens-500, min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{% if scene.num_history_entries < 25 %}{{ scene.description.replace("\r\n","\n") }}{% endif -%}
|
||||
{% for scene_context in scene_context_history -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<|SECTION:TASK|>
|
||||
Generate a real world character profile for {{ name }}, one attribute per line.
|
||||
{{ set_prepared_response("Name: "+name+"\nAge:") }}
|
||||
@@ -0,0 +1,13 @@
|
||||
<|SECTION:CONTENT|>
|
||||
{% if text -%}
|
||||
{{ text }}
|
||||
{% else -%}
|
||||
{% set scene_context_history = scene.context_history(budget=max_tokens-500, min_dialogue=25, sections=False, keep_director=True) -%}
|
||||
{% if scene.num_history_entries < 25 %}{{ scene.description }}{% endif -%}
|
||||
{% for scene_context in scene_context_history -%}
|
||||
{{ scene_context }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<|SECTION:TASK|>
|
||||
Identify all main characters by name respond with a json object in the format of {"characters":[{"name": "John" , "description": "Information about the character" }]}
|
||||
{{ set_json_response({"characters":[""]}) }}
|
||||
@@ -41,7 +41,7 @@ Instruction to the Analyst:
|
||||
6. Be factual and truthful. Don't make up things that are not in the context or dialogue.
|
||||
7. Snapshot text should always be specified. If you don't know what to write, write "You see nothing special."
|
||||
|
||||
Required response: a valid JSON response according to the JSON example containing lists of items and characters.
|
||||
Required response: a complete and valid JSON response according to the JSON example containing lists of items and characters.
|
||||
|
||||
characters should habe the following attributes: `name`, `emotion`, `snapshot`
|
||||
items should have the following attributes: `name`, `snapshot`
|
||||
@@ -75,6 +75,10 @@ class CharacterMessage(SceneMessage):
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
@property
|
||||
def character_name(self):
|
||||
return self.message.split(":", 1)[0]
|
||||
|
||||
@dataclass
|
||||
class NarratorMessage(SceneMessage):
|
||||
source: str = "progress_story"
|
||||
|
||||
@@ -8,6 +8,8 @@ import structlog
|
||||
from talemate.prompts import Prompt
|
||||
from talemate.tale_mate import Character, Actor, Player
|
||||
|
||||
from typing import Union
|
||||
|
||||
log = structlog.get_logger("talemate.server.character_creator")
|
||||
|
||||
|
||||
@@ -18,7 +20,7 @@ class StepData(pydantic.BaseModel):
|
||||
character_prompt: str
|
||||
dialogue_guide: str
|
||||
dialogue_examples: list[str]
|
||||
base_attributes: dict[str, str] = {}
|
||||
base_attributes: dict[str, Union[str, int]] = {}
|
||||
custom_attributes: dict[str, str] = {}
|
||||
details: dict[str, str] = {}
|
||||
description: str = None
|
||||
|
||||
@@ -3,6 +3,7 @@ import pydantic
|
||||
import asyncio
|
||||
import structlog
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
from talemate.load import load_character_into_scene
|
||||
|
||||
@@ -12,11 +13,11 @@ class ListScenesData(pydantic.BaseModel):
|
||||
scene_path: str
|
||||
|
||||
class CreateSceneData(pydantic.BaseModel):
|
||||
name: str = None
|
||||
description: str = None
|
||||
intro: str = None
|
||||
content_context: str = None
|
||||
prompt: str = None
|
||||
name: Union[str, None] = None
|
||||
description: Union[str, None] = None
|
||||
intro: Union[str, None] = None
|
||||
content_context: Union[str, None] = None
|
||||
prompt: Union[str, None] = None
|
||||
|
||||
class SceneCreatorServerPlugin:
|
||||
|
||||
|
||||
@@ -101,7 +101,9 @@ class WebsocketHandler(Receiver):
|
||||
|
||||
log.debug("Linked agent", agent_typ=agent_typ, client=client.name)
|
||||
agent = instance.get_agent(agent_typ, client=client)
|
||||
agent.client = client
|
||||
agent.client = client
|
||||
agent.apply_config(**agent_config)
|
||||
|
||||
|
||||
instance.emit_agents_status()
|
||||
|
||||
@@ -238,11 +240,18 @@ class WebsocketHandler(Receiver):
|
||||
"client": self.llm_clients[agent["client"]]["name"],
|
||||
"name": name,
|
||||
}
|
||||
|
||||
|
||||
agent_instance = instance.get_agent(name, **self.agents[name])
|
||||
agent_instance.client = self.llm_clients[agent["client"]]["client"]
|
||||
|
||||
if agent_instance.has_toggle:
|
||||
self.agents[name]["enabled"] = agent["enabled"]
|
||||
|
||||
if getattr(agent_instance, "actions", None):
|
||||
self.agents[name]["actions"] = agent.get("actions", {})
|
||||
|
||||
agent_instance.apply_config(**self.agents[name])
|
||||
|
||||
log.debug("Configured agent", name=name, client_name=self.llm_clients[agent["client"]]["name"], client=self.llm_clients[agent["client"]]["client"])
|
||||
|
||||
self.config["agents"] = self.agents
|
||||
@@ -585,5 +594,12 @@ class WebsocketHandler(Receiver):
|
||||
plugin = self.routes[route]
|
||||
try:
|
||||
await plugin.handle(data)
|
||||
except Exception:
|
||||
log.error("route", error=traceback.format_exc())
|
||||
except Exception as e:
|
||||
log.error("route", error=traceback.format_exc())
|
||||
self.queue_put(
|
||||
{
|
||||
"plugin": plugin.router,
|
||||
"type": "error",
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
@@ -18,6 +18,7 @@ import talemate.events as events
|
||||
import talemate.util as util
|
||||
import talemate.save as save
|
||||
from talemate.emit import Emitter, emit, wait_for_input
|
||||
import talemate.emit.async_signals as async_signals
|
||||
from talemate.util import colored_text, count_tokens, extract_metadata, wrap_text
|
||||
from talemate.scene_message import SceneMessage, CharacterMessage, DirectorMessage, NarratorMessage, TimePassageMessage
|
||||
from talemate.exceptions import ExitScene, RestartSceneLoop, ResetScene, TalemateError, TalemateInterrupt, LLMAccuracyError
|
||||
@@ -49,8 +50,8 @@ class Character:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
greeting_text: str,
|
||||
description: str = "",
|
||||
greeting_text: str = "",
|
||||
gender: str = "female",
|
||||
color: str = "cyan",
|
||||
example_dialogue: List[str] = [],
|
||||
@@ -350,6 +351,9 @@ class Character:
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
|
||||
if attr.lower() in ["name", "scenario_context", "_prompt", "_template"]:
|
||||
continue
|
||||
|
||||
items.append({
|
||||
"text": f"{self.name}'s {attr}: {value}",
|
||||
"id": f"{self.name}.{attr}",
|
||||
@@ -506,6 +510,8 @@ class Player(Actor):
|
||||
|
||||
if not commands.Manager.is_command(message):
|
||||
|
||||
message = util.ensure_dialog_format(message)
|
||||
|
||||
self.message = message
|
||||
|
||||
self.scene.push_history(
|
||||
@@ -515,7 +521,7 @@ class Player(Actor):
|
||||
|
||||
return message
|
||||
|
||||
|
||||
async_signals.register("game_loop")
|
||||
|
||||
class Scene(Emitter):
|
||||
"""
|
||||
@@ -538,6 +544,7 @@ class Scene(Emitter):
|
||||
self.main_character = None
|
||||
self.static_tokens = 0
|
||||
self.max_tokens = 2048
|
||||
self.next_actor = None
|
||||
|
||||
self.name = ""
|
||||
self.filename = ""
|
||||
@@ -561,6 +568,7 @@ class Scene(Emitter):
|
||||
"history_add": signal("history_add"),
|
||||
"archive_add": signal("archive_add"),
|
||||
"character_state": signal("character_state"),
|
||||
"game_loop": async_signals.get("game_loop"),
|
||||
}
|
||||
|
||||
self.setup_emitter(scene=self)
|
||||
@@ -571,6 +579,10 @@ class Scene(Emitter):
|
||||
def characters(self):
|
||||
for actor in self.actors:
|
||||
yield actor.character
|
||||
|
||||
@property
|
||||
def character_names(self):
|
||||
return [character.name for character in self.characters]
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
@@ -586,6 +598,20 @@ class Scene(Emitter):
|
||||
def project_name(self):
|
||||
return self.name.replace(" ", "-").replace("'","").lower()
|
||||
|
||||
@property
|
||||
def num_history_entries(self):
|
||||
return len(self.history)
|
||||
|
||||
@property
|
||||
def prev_actor(self):
|
||||
# will find the first CharacterMessage in history going from the end
|
||||
# and return the character name attached to it to determine the actor
|
||||
# that most recently spoke
|
||||
|
||||
for idx in range(len(self.history) - 1, -1, -1):
|
||||
if isinstance(self.history[idx], CharacterMessage):
|
||||
return self.history[idx].character_name
|
||||
|
||||
def apply_scene_config(self, scene_config:dict):
|
||||
scene_config = SceneConfig(**scene_config)
|
||||
|
||||
@@ -872,6 +898,7 @@ class Scene(Emitter):
|
||||
else:
|
||||
end = 0
|
||||
|
||||
|
||||
history_length = len(self.history)
|
||||
|
||||
# we then take the history from the end index to the end of the history
|
||||
@@ -883,7 +910,7 @@ class Scene(Emitter):
|
||||
dialogue = self.history[end:]
|
||||
else:
|
||||
dialogue = self.history[end:-dialogue_negative_offset]
|
||||
|
||||
|
||||
if not keep_director:
|
||||
dialogue = [line for line in dialogue if not isinstance(line, DirectorMessage)]
|
||||
|
||||
@@ -892,20 +919,7 @@ class Scene(Emitter):
|
||||
|
||||
if dialogue and insert_bot_token is not None:
|
||||
dialogue.insert(-insert_bot_token, "<|BOT|>")
|
||||
|
||||
if dialogue:
|
||||
context_history = ["<|SECTION:DIALOGUE|>","\n".join(map(str, dialogue)), "<|CLOSE_SECTION|>"]
|
||||
else:
|
||||
context_history = []
|
||||
|
||||
if not sections and context_history:
|
||||
context_history = [context_history[1]]
|
||||
|
||||
# if we dont have lots of archived history, we can also include the scene
|
||||
# description at tbe beginning of the context history
|
||||
|
||||
archive_insert_idx = 0
|
||||
|
||||
# iterate backwards through archived history and count how many entries
|
||||
# there are that have an end index
|
||||
num_archived_entries = 0
|
||||
@@ -914,10 +928,37 @@ class Scene(Emitter):
|
||||
if self.archived_history[i].get("end") is None:
|
||||
break
|
||||
num_archived_entries += 1
|
||||
|
||||
if num_archived_entries <= 2 and add_archieved_history:
|
||||
|
||||
show_intro = num_archived_entries <= 2 and add_archieved_history
|
||||
reserved_min_archived_history_tokens = count_tokens(self.archived_history[-1]["text"]) if self.archived_history else 0
|
||||
reserved_intro_tokens = count_tokens(self.get_intro()) if show_intro else 0
|
||||
|
||||
max_dialogue_budget = min(max(budget - reserved_intro_tokens - reserved_min_archived_history_tokens, 1000), budget)
|
||||
|
||||
dialogue_popped = False
|
||||
while count_tokens(dialogue) > max_dialogue_budget:
|
||||
dialogue.pop(0)
|
||||
dialogue_popped = True
|
||||
|
||||
if dialogue:
|
||||
context_history = ["<|SECTION:DIALOGUE|>","\n".join(map(str, dialogue)), "<|CLOSE_SECTION|>"]
|
||||
else:
|
||||
context_history = []
|
||||
|
||||
if not sections and context_history:
|
||||
context_history = [context_history[1]]
|
||||
|
||||
# we only have room for dialogue, so we return it
|
||||
if dialogue_popped:
|
||||
return context_history
|
||||
|
||||
# if we dont have lots of archived history, we can also include the scene
|
||||
# description at tbe beginning of the context history
|
||||
|
||||
archive_insert_idx = 0
|
||||
|
||||
if show_intro:
|
||||
|
||||
for character in self.characters:
|
||||
if character.greeting_text and character.greeting_text != self.get_intro():
|
||||
context_history.insert(0, character.greeting_text)
|
||||
@@ -1238,13 +1279,22 @@ class Scene(Emitter):
|
||||
|
||||
# sort self.actors by actor.character.is_player, making is_player the first element
|
||||
self.actors.sort(key=lambda x: x.character.is_player, reverse=True)
|
||||
|
||||
self.active_actor = None
|
||||
self.next_actor = None
|
||||
|
||||
while continue_scene:
|
||||
|
||||
try:
|
||||
|
||||
await self.signals["game_loop"].send(events.GameLoopEvent(scene=self, event_type="game_loop"))
|
||||
|
||||
for actor in self.actors:
|
||||
|
||||
if self.next_actor and actor.character.name != self.next_actor:
|
||||
self.log.debug(f"Skipping actor", actor=actor.character.name, next_actor=self.next_actor)
|
||||
continue
|
||||
|
||||
self.active_actor = actor
|
||||
|
||||
if not actor.character.is_player:
|
||||
@@ -1261,7 +1311,7 @@ class Scene(Emitter):
|
||||
break
|
||||
await self.call_automated_actions()
|
||||
continue
|
||||
|
||||
|
||||
# Store the most recent AI Actor
|
||||
self.most_recent_ai_actor = actor
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import structlog
|
||||
import isodate
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
from thefuzz import fuzz
|
||||
from colorama import Back, Fore, Style, init
|
||||
from PIL import Image
|
||||
|
||||
@@ -345,7 +345,14 @@ def clean_paragraph(paragraph: str) -> str:
|
||||
return cleaned_text
|
||||
|
||||
|
||||
def clean_dialogue(dialogue: str, main_name: str = None) -> str:
|
||||
def clean_message(message: str) -> str:
|
||||
message = message.strip()
|
||||
message = re.sub(r"\s+", " ", message)
|
||||
message = message.replace("(", "*").replace(")", "*")
|
||||
message = message.replace("[", "*").replace("]", "*")
|
||||
return message
|
||||
|
||||
def clean_dialogue_old(dialogue: str, main_name: str = None) -> str:
|
||||
"""
|
||||
Cleans up generated dialogue by removing unnecessary whitespace and newlines.
|
||||
|
||||
@@ -356,12 +363,7 @@ def clean_dialogue(dialogue: str, main_name: str = None) -> str:
|
||||
str: The cleaned dialogue.
|
||||
"""
|
||||
|
||||
def clean_message(message: str) -> str:
|
||||
message = message.strip().strip('"')
|
||||
message = re.sub(r"\s+", " ", message)
|
||||
message = message.replace("(", "*").replace(")", "*")
|
||||
message = message.replace("[", "*").replace("]", "*")
|
||||
return message
|
||||
|
||||
|
||||
cleaned_lines = []
|
||||
current_name = None
|
||||
@@ -373,6 +375,9 @@ def clean_dialogue(dialogue: str, main_name: str = None) -> str:
|
||||
if ":" in line:
|
||||
name, message = line.split(":", 1)
|
||||
name = name.strip()
|
||||
if name != main_name:
|
||||
break
|
||||
|
||||
message = clean_message(message)
|
||||
|
||||
if not message:
|
||||
@@ -391,6 +396,45 @@ def clean_dialogue(dialogue: str, main_name: str = None) -> str:
|
||||
cleaned_dialogue = "\n".join(cleaned_lines)
|
||||
return cleaned_dialogue
|
||||
|
||||
def clean_dialogue(dialogue: str, main_name: str) -> str:
|
||||
|
||||
# keep spliting the dialogue by : with a max count of 1
|
||||
# until the left side is no longer the main name
|
||||
|
||||
cleaned_dialogue = ""
|
||||
|
||||
# find all occurances of : and then walk backwards
|
||||
# and mark the first one that isnt preceded by the {main_name}
|
||||
cutoff = -1
|
||||
log.debug("clean_dialogue", dialogue=dialogue, main_name=main_name)
|
||||
for match in re.finditer(r":", dialogue, re.MULTILINE):
|
||||
index = match.start()
|
||||
check = dialogue[index-len(main_name):index]
|
||||
log.debug("clean_dialogue", check=check, main_name=main_name)
|
||||
if check != main_name:
|
||||
cutoff = index
|
||||
break
|
||||
|
||||
# then split dialogue at the index and return on only
|
||||
# the left side
|
||||
|
||||
if cutoff > -1:
|
||||
log.debug("clean_dialogue", index=index)
|
||||
cleaned_dialogue = dialogue[:index]
|
||||
cleaned_dialogue = strip_partial_sentences(cleaned_dialogue)
|
||||
|
||||
# remove all occurances of "{main_name}: " and then prepend it once
|
||||
|
||||
cleaned_dialogue = cleaned_dialogue.replace(f"{main_name}: ", "")
|
||||
cleaned_dialogue = f"{main_name}: {cleaned_dialogue}"
|
||||
|
||||
return clean_message(cleaned_dialogue)
|
||||
|
||||
dialogue = dialogue.replace(f"{main_name}: ", "")
|
||||
dialogue = f"{main_name}: {dialogue}"
|
||||
|
||||
return clean_message(strip_partial_sentences(dialogue))
|
||||
|
||||
|
||||
def clean_attribute(attribute: str) -> str:
|
||||
"""
|
||||
@@ -442,18 +486,6 @@ def clean_attribute(attribute: str) -> str:
|
||||
return attribute.strip()
|
||||
|
||||
|
||||
def fix_faulty_json(data: str) -> str:
|
||||
# Fix missing commas
|
||||
data = re.sub(r'}\s*{', '},{', data)
|
||||
data = re.sub(r']\s*{', '],{', data)
|
||||
data = re.sub(r'}\s*\[', '},{', data)
|
||||
data = re.sub(r']\s*\[', '],[', data)
|
||||
|
||||
# Fix trailing commas
|
||||
data = re.sub(r',\s*}', '}', data)
|
||||
data = re.sub(r',\s*]', ']', data)
|
||||
|
||||
return data
|
||||
|
||||
def duration_to_timedelta(duration):
|
||||
"""Convert an isodate.Duration object to a datetime.timedelta object."""
|
||||
@@ -594,4 +626,239 @@ def iso8601_correct_duration(duration: str) -> str:
|
||||
if time_component:
|
||||
corrected_duration += "T" + time_component
|
||||
|
||||
return corrected_duration
|
||||
return corrected_duration
|
||||
|
||||
|
||||
def fix_faulty_json(data: str) -> str:
|
||||
# Fix missing commas
|
||||
data = re.sub(r'}\s*{', '},{', data)
|
||||
data = re.sub(r']\s*{', '],{', data)
|
||||
data = re.sub(r'}\s*\[', '},{', data)
|
||||
data = re.sub(r']\s*\[', '],[', data)
|
||||
|
||||
# Fix trailing commas
|
||||
data = re.sub(r',\s*}', '}', data)
|
||||
data = re.sub(r',\s*]', ']', data)
|
||||
|
||||
try:
|
||||
json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
json.loads(data+"}")
|
||||
return data+"}"
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
json.loads(data+"]")
|
||||
return data+"]"
|
||||
except json.JSONDecodeError:
|
||||
return data
|
||||
|
||||
return data
|
||||
|
||||
def extract_json(s):
|
||||
"""
|
||||
Extracts a JSON string from the beginning of the input string `s`.
|
||||
|
||||
Parameters:
|
||||
s (str): The input string containing a JSON string at the beginning.
|
||||
|
||||
Returns:
|
||||
str: The extracted JSON string.
|
||||
dict: The parsed JSON object.
|
||||
|
||||
Raises:
|
||||
ValueError: If a valid JSON string is not found.
|
||||
"""
|
||||
open_brackets = 0
|
||||
close_brackets = 0
|
||||
bracket_stack = []
|
||||
json_string_start = None
|
||||
s = s.lstrip() # Strip white spaces and line breaks from the beginning
|
||||
i = 0
|
||||
|
||||
log.debug("extract_json", s=s)
|
||||
|
||||
# Iterate through the string.
|
||||
while i < len(s):
|
||||
# Count the opening and closing curly brackets.
|
||||
if s[i] == '{' or s[i] == '[':
|
||||
bracket_stack.append(s[i])
|
||||
open_brackets += 1
|
||||
if json_string_start is None:
|
||||
json_string_start = i
|
||||
elif s[i] == '}' or s[i] == ']':
|
||||
bracket_stack
|
||||
close_brackets += 1
|
||||
# Check if the brackets match, indicating a complete JSON string.
|
||||
if open_brackets == close_brackets:
|
||||
json_string = s[json_string_start:i+1]
|
||||
# Try to parse the JSON string.
|
||||
return json_string, json.loads(json_string)
|
||||
i += 1
|
||||
|
||||
if json_string_start is None:
|
||||
raise ValueError("No JSON string found.")
|
||||
|
||||
json_string = s[json_string_start:]
|
||||
while bracket_stack:
|
||||
char = bracket_stack.pop()
|
||||
if char == '{':
|
||||
json_string += '}'
|
||||
elif char == '[':
|
||||
json_string += ']'
|
||||
|
||||
json_object = json.loads(json_string)
|
||||
return json_string, json_object
|
||||
|
||||
def dedupe_string(s: str, min_length: int = 32, similarity_threshold: int = 95, debug: bool = False) -> str:
|
||||
|
||||
"""
|
||||
Removes duplicate lines from a string.
|
||||
|
||||
Parameters:
|
||||
s (str): The input string.
|
||||
min_length (int): The minimum length of a line to be checked for duplicates.
|
||||
similarity_threshold (int): The similarity threshold to use when comparing lines.
|
||||
debug (bool): Whether to log debug messages.
|
||||
|
||||
Returns:
|
||||
str: The deduplicated string.
|
||||
"""
|
||||
|
||||
lines = s.split("\n")
|
||||
deduped = []
|
||||
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if len(stripped_line) > min_length:
|
||||
similar_found = False
|
||||
for existing_line in deduped:
|
||||
similarity = fuzz.ratio(stripped_line, existing_line.strip())
|
||||
if similarity >= similarity_threshold:
|
||||
similar_found = True
|
||||
if debug:
|
||||
log.debug("DEDUPE", similarity=similarity, line=line, existing_line=existing_line)
|
||||
break
|
||||
if not similar_found:
|
||||
deduped.append(line)
|
||||
else:
|
||||
deduped.append(line) # Allow shorter strings without dupe check
|
||||
|
||||
return "\n".join(deduped)
|
||||
|
||||
def remove_extra_linebreaks(s: str) -> str:
|
||||
"""
|
||||
Removes extra line breaks from a string.
|
||||
|
||||
Parameters:
|
||||
s (str): The input string.
|
||||
|
||||
Returns:
|
||||
str: The string with extra line breaks removed.
|
||||
"""
|
||||
return re.sub(r"\n{3,}", "\n\n", s)
|
||||
|
||||
def replace_exposition_markers(s:str) -> str:
|
||||
s = s.replace("(", "*").replace(")", "*")
|
||||
s = s.replace("[", "*").replace("]", "*")
|
||||
return s
|
||||
|
||||
|
||||
def ensure_dialog_format(line:str, talking_character:str=None) -> str:
|
||||
|
||||
line = mark_exposition(line, talking_character)
|
||||
line = mark_spoken_words(line, talking_character)
|
||||
return line
|
||||
|
||||
|
||||
def mark_spoken_words(line:str, talking_character:str=None) -> str:
|
||||
# if there are no asterisks in the line, it means its impossible to tell
|
||||
# dialogue apart from exposition
|
||||
if "*" not in line:
|
||||
return line
|
||||
|
||||
if talking_character and line.startswith(f"{talking_character}:"):
|
||||
line = line[len(talking_character)+1:].lstrip()
|
||||
|
||||
|
||||
# Splitting the text into segments based on asterisks
|
||||
segments = re.split('(\*[^*]*\*)', line)
|
||||
formatted_line = ""
|
||||
|
||||
for i, segment in enumerate(segments):
|
||||
if segment.startswith("*") and segment.endswith("*"):
|
||||
# If the segment is an action or thought, add it as is
|
||||
formatted_line += segment
|
||||
else:
|
||||
# For non-action/thought parts, trim and add quotes only if not empty and not already quoted
|
||||
trimmed_segment = segment.strip()
|
||||
if trimmed_segment:
|
||||
if not (trimmed_segment.startswith('"') and trimmed_segment.endswith('"')):
|
||||
formatted_line += f' "{trimmed_segment}"'
|
||||
else:
|
||||
formatted_line += f' {trimmed_segment}'
|
||||
|
||||
|
||||
# adds spaces betwen *" and "* to make it easier to read
|
||||
formatted_line = formatted_line.replace('*"', '* "')
|
||||
formatted_line = formatted_line.replace('"*', '" *')
|
||||
|
||||
if talking_character:
|
||||
formatted_line = f"{talking_character}: {formatted_line}"
|
||||
|
||||
log.debug("mark_spoken_words", line=line, formatted_line=formatted_line)
|
||||
|
||||
return formatted_line.strip() # Trim any leading/trailing whitespace
|
||||
|
||||
|
||||
def mark_exposition(line:str, talking_character:str=None) -> str:
|
||||
"""
|
||||
Will loop through the string and make sure chunks outside of "" are marked with *.
|
||||
|
||||
For example:
|
||||
|
||||
"No, you're not wrong" sips his wine "This tastes gross." coughs "acquired taste i guess?"
|
||||
|
||||
becomes
|
||||
|
||||
"No, you're not wrong" *sips his wine* "This tastes gross." *coughs* "acquired taste i guess?"
|
||||
"""
|
||||
|
||||
# no quotes in string, means its impossible to tell dialogue apart from exposition
|
||||
if '"' not in line:
|
||||
return line
|
||||
|
||||
if talking_character and line.startswith(f"{talking_character}:"):
|
||||
line = line[len(talking_character)+1:].lstrip()
|
||||
|
||||
# Splitting the text into segments based on quotes
|
||||
segments = re.split('("[^"]*")', line)
|
||||
formatted_line = ""
|
||||
|
||||
for i, segment in enumerate(segments):
|
||||
# If the segment is a spoken part (inside quotes), add it as is
|
||||
if segment.startswith('"') and segment.endswith('"'):
|
||||
formatted_line += segment
|
||||
else:
|
||||
# Split the non-spoken segment into sub-segments based on existing asterisks
|
||||
sub_segments = re.split('(\*[^*]*\*)', segment)
|
||||
for sub_segment in sub_segments:
|
||||
if sub_segment.startswith("*") and sub_segment.endswith("*"):
|
||||
# If the sub-segment is already formatted, add it as is
|
||||
formatted_line += sub_segment
|
||||
else:
|
||||
# Trim and add asterisks only to non-empty sub-segments
|
||||
trimmed_sub_segment = sub_segment.strip()
|
||||
if trimmed_sub_segment:
|
||||
formatted_line += f" *{trimmed_sub_segment}*"
|
||||
|
||||
# adds spaces betwen *" and "* to make it easier to read
|
||||
formatted_line = formatted_line.replace('*"', '* "')
|
||||
formatted_line = formatted_line.replace('"*', '" *')
|
||||
|
||||
if talking_character:
|
||||
formatted_line = f"{talking_character}: {formatted_line}"
|
||||
log.debug("mark_exposition", line=line, formatted_line=formatted_line)
|
||||
|
||||
|
||||
return formatted_line.strip() # Trim any leading/trailing whitespace
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from talemate.emit import emit
|
||||
import structlog
|
||||
from typing import Union
|
||||
|
||||
import talemate.instance as instance
|
||||
from talemate.prompts import Prompt
|
||||
@@ -9,11 +10,11 @@ import talemate.automated_action as automated_action
|
||||
log = structlog.get_logger("talemate")
|
||||
|
||||
class CharacterState(BaseModel):
|
||||
snapshot: str = None
|
||||
emotion: str = None
|
||||
snapshot: Union[str, None] = None
|
||||
emotion: Union[str, None] = None
|
||||
|
||||
class ObjectState(BaseModel):
|
||||
snapshot: str = None
|
||||
snapshot: Union[str, None] = None
|
||||
|
||||
class WorldState(BaseModel):
|
||||
|
||||
@@ -24,15 +25,15 @@ class WorldState(BaseModel):
|
||||
items: dict[str, ObjectState] = {}
|
||||
|
||||
# location description
|
||||
location: str = None
|
||||
location: Union[str, None] = None
|
||||
|
||||
@property
|
||||
def agent(self):
|
||||
return instance.get_agent("summarizer")
|
||||
return instance.get_agent("world_state")
|
||||
|
||||
@property
|
||||
def pretty_json(self):
|
||||
return self.json(indent=2)
|
||||
return self.model_dump_json(indent=2)
|
||||
|
||||
@property
|
||||
def as_list(self):
|
||||
@@ -93,11 +94,4 @@ class WorldState(BaseModel):
|
||||
"items": self.items,
|
||||
"location": self.location,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automated_action.register("world_state", frequency=5, call_initially=False)
|
||||
class WorldStateAction(automated_action.AutomatedAction):
|
||||
async def action(self):
|
||||
await self.scene.world_state.request_update()
|
||||
return True
|
||||
)
|
||||
@@ -11,7 +11,11 @@
|
||||
<span class="ml-1" v-if="agent.label"> {{ agent.label }}</span>
|
||||
<span class="ml-1" v-else> {{ agent.name }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ agent.client }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>
|
||||
{{ agent.client }}
|
||||
</v-list-item-subtitle>
|
||||
<v-chip class="mr-1" v-if="agent.status === 'disabled'" size="x-small">Disabled</v-chip>
|
||||
<v-chip v-if="agent.data.experimental" color="warning" size="x-small">experimental</v-chip>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<AgentModal :dialog="dialog" :formTitle="formTitle" @save="saveAgent" @update:dialog="updateDialog"></AgentModal>
|
||||
@@ -116,6 +120,7 @@ export default {
|
||||
handleMessage(data) {
|
||||
// Handle agent_status message type
|
||||
if (data.type === 'agent_status') {
|
||||
console.log("agents: got agent_status message", data)
|
||||
// Find the client with the given name
|
||||
const agent = this.state.agents.find(agent => agent.name === data.name);
|
||||
if (agent) {
|
||||
@@ -124,15 +129,27 @@ export default {
|
||||
agent.data = data.data;
|
||||
agent.status = data.status;
|
||||
agent.label = data.message;
|
||||
agent.actions = {}
|
||||
for(let i in data.data.actions) {
|
||||
agent.actions[i] = {enabled: data.data.actions[i].enabled, config: data.data.actions[i].config};
|
||||
}
|
||||
agent.enabled = data.data.enabled;
|
||||
} else {
|
||||
// Add the agent to the list of agents
|
||||
let actions = {}
|
||||
for(let i in data.data.actions) {
|
||||
actions[i] = {enabled: data.data.actions[i].enabled, config: data.data.actions[i].config};
|
||||
}
|
||||
this.state.agents.push({
|
||||
name: data.name,
|
||||
client: data.client,
|
||||
status: data.status,
|
||||
data: data.data,
|
||||
label: data.message,
|
||||
actions: actions,
|
||||
enabled: data.data.enabled,
|
||||
});
|
||||
console.log("agents: added new agent", this.state.agents[this.state.agents.length - 1], data)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,53 @@
|
||||
<template>
|
||||
<v-dialog v-model="localDialog" persistent max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ formTitle }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="agent.name" readonly label="Agent"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-select v-model="agent.client" :items="agent.data.client" label="Client"></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="close">Close</v-btn>
|
||||
<v-btn color="blue darken-1" text @click="save">Save</v-btn>
|
||||
</v-card-actions>
|
||||
<v-dialog v-model="localDialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="9">
|
||||
<v-icon>mdi-transit-connection-variant</v-icon>
|
||||
{{ agent.label }}
|
||||
</v-col>
|
||||
<v-col cols="3" class="text-right">
|
||||
<v-checkbox :label="enabledLabel()" hide-details density="compact" color="green" v-model="agent.enabled"
|
||||
v-if="agent.data.has_toggle"></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select v-model="agent.client" :items="agent.data.client" label="Client"></v-select>
|
||||
|
||||
<v-alert type="warning" variant="tonal" density="compact" v-if="agent.data.experimental">
|
||||
This agent is currently experimental and may significantly decrease performance and / or require
|
||||
strong LLMs to function properly.
|
||||
</v-alert>
|
||||
|
||||
<v-card v-for="(action, key) in agent.actions" :key="key" density="compact">
|
||||
<v-card-subtitle>
|
||||
<v-checkbox :label="agent.data.actions[key].label" hide-details density="compact" color="green" v-model="action.enabled"></v-checkbox>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
{{ agent.data.actions[key].description }}
|
||||
<div v-for="(action_config, config_key) in agent.data.actions[key].config" :key="config_key">
|
||||
<!-- render config widgets based on action_config.type (int, str, bool, float) -->
|
||||
<v-text-field v-if="action_config.type === 'str'" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact"></v-text-field>
|
||||
<v-slider v-if="action_config.type === 'number' && action_config.step !== null" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" :min="action_config.min" :max="action_config.max" :step="action_config.step" density="compact" thumb-label></v-slider>
|
||||
<v-checkbox v-if="action_config.type === 'bool'" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact"></v-checkbox>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="close">Close</v-btn>
|
||||
<v-btn color="primary" @click="save">Save</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -56,6 +81,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
enabledLabel() {
|
||||
if (this.agent.enabled) {
|
||||
return 'Enabled';
|
||||
} else {
|
||||
return 'Disabled';
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('update:dialog', false);
|
||||
},
|
||||
|
||||
@@ -104,17 +104,17 @@
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(question, index) in detail_questions" :key="index">
|
||||
<v-list-item-title class="text-capitalize">
|
||||
<div>
|
||||
<v-icon color="red" @click="detail_questions.splice(index, 1)">mdi-delete</v-icon>
|
||||
{{ question }}
|
||||
</v-list-item-title>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<v-text-field label="Custom question" v-model="new_question" @keydown.prevent.enter="addQuestion()"></v-text-field>
|
||||
</v-list>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(value, question) in details" :key="question">
|
||||
<v-list-item-title class="text-capitalize">{{ question }}</v-list-item-title>
|
||||
<v-list-item-title>{{ question }}</v-list-item-title>
|
||||
<v-textarea rows="1" auto-grow v-model="details[question]"></v-textarea>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -135,10 +135,10 @@
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(example, index) in dialogue_examples" :key="index">
|
||||
<v-list-item-title class="text-capitalize">
|
||||
<div>
|
||||
<v-icon color="red" @click="dialogue_examples.splice(index, 1)">mdi-delete</v-icon>
|
||||
{{ example }}
|
||||
</v-list-item-title>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<v-text-field label="Add dialogue example" v-model="new_dialogue_example" @keydown.prevent.enter="addDialogueExample()"></v-text-field>
|
||||
</v-list>
|
||||
@@ -163,6 +163,7 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
<v-alert v-if="error_message !== null" type="error" variant="tonal" density="compact" class="mb-2">{{ error_message }}</v-alert>
|
||||
|
||||
</v-stepper>
|
||||
</v-window>
|
||||
@@ -218,6 +219,8 @@ export default {
|
||||
custom_attributes: {},
|
||||
new_attribute_name: "",
|
||||
new_attribute_instruction: "",
|
||||
|
||||
error_message: null,
|
||||
}
|
||||
},
|
||||
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets'],
|
||||
@@ -276,6 +279,7 @@ export default {
|
||||
this.dialogue_examples = [];
|
||||
this.character = null;
|
||||
this.generating = false;
|
||||
this.error_message = null;
|
||||
},
|
||||
|
||||
addQuestion() {
|
||||
@@ -380,6 +384,8 @@ export default {
|
||||
if(step == 4)
|
||||
this.details = {};
|
||||
|
||||
this.error_message = null;
|
||||
|
||||
this.sendRequest({
|
||||
action: 'submit',
|
||||
base_attributes: this.base_attributes,
|
||||
@@ -422,6 +428,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
hanldeError(error_message) {
|
||||
this.generating = false;
|
||||
this.error_message = error_message;
|
||||
},
|
||||
|
||||
handleBaseAttribute(data) {
|
||||
this.base_attributes[data.name] = data.value;
|
||||
if(data.name == "name") {
|
||||
@@ -461,6 +472,8 @@ export default {
|
||||
} else if(data.action === 'description') {
|
||||
this.description = data.description;
|
||||
}
|
||||
} else if(data.type === "error" && data.plugin === 'character_creator') {
|
||||
this.hanldeError(data.error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="expanded">
|
||||
<v-img @click="toggle()" v-if="asset_id !== null" :src="'data:'+media_type+';base64, '+base64"></v-img>
|
||||
<v-img cover @click="toggle()" v-if="asset_id !== null" :src="'data:'+media_type+';base64, '+base64"></v-img>
|
||||
</div>
|
||||
<v-list-subheader v-else @click="toggle()"><v-icon>mdi-image-frame</v-icon> Cover image
|
||||
<v-icon v-if="expanded" icon="mdi-chevron-down"></v-icon>
|
||||
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
{"value" : "P1D", "title": "1 day"},
|
||||
{"value" : "PT8H", "title": "8 hours"},
|
||||
{"value" : "PT4H", "title": "4 hours"},
|
||||
{"Value" : "PT1H", "title": "1 hour"},
|
||||
{"value" : "PT1H", "title": "1 hour"},
|
||||
{"value" : "PT30M", "title": "30 minutes"},
|
||||
{"value" : "PT15M", "title": "15 minutes"}
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<LoadScene ref="loadScene" />
|
||||
<v-divider></v-divider>
|
||||
<div :style="(sceneActive && scene.environment === 'scene' ? 'display:block' : 'display:none')">
|
||||
<GameOptions v-if="sceneActive" ref="gameOptions" />
|
||||
<!-- <GameOptions v-if="sceneActive" ref="gameOptions" /> -->
|
||||
<v-divider></v-divider>
|
||||
<CoverImage v-if="sceneActive" ref="coverImage" />
|
||||
<WorldState v-if="sceneActive" ref="worldState" />
|
||||
@@ -153,7 +153,7 @@ import LoadScene from './LoadScene.vue';
|
||||
import SceneTools from './SceneTools.vue';
|
||||
import SceneMessages from './SceneMessages.vue';
|
||||
import WorldState from './WorldState.vue';
|
||||
import GameOptions from './GameOptions.vue';
|
||||
//import GameOptions from './GameOptions.vue';
|
||||
import CoverImage from './CoverImage.vue';
|
||||
import CharacterSheet from './CharacterSheet.vue';
|
||||
import SceneHistory from './SceneHistory.vue';
|
||||
@@ -169,7 +169,7 @@ export default {
|
||||
SceneTools,
|
||||
SceneMessages,
|
||||
WorldState,
|
||||
GameOptions,
|
||||
//GameOptions,
|
||||
CoverImage,
|
||||
CharacterSheet,
|
||||
SceneHistory,
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else text="Make this character real, adding it to the scene as an actor.">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" @click.stop="persistCharacter(name)" icon="mdi-chat-plus-outline"></v-btn>
|
||||
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-divider class="mt-1"></v-divider>
|
||||
</v-expansion-panel-text>
|
||||
@@ -97,6 +103,12 @@ export default {
|
||||
text: `!narrate_c:${name}`,
|
||||
}));
|
||||
},
|
||||
persistCharacter(name) {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
text: `!pc:${name}`,
|
||||
}));
|
||||
},
|
||||
lookAtItem(name) {
|
||||
this.getWebsocket().send(JSON.stringify({
|
||||
type: 'interact',
|
||||
|
||||
4
templates/llm-prompt/Alpaca.jinja2
Normal file
4
templates/llm-prompt/Alpaca.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ system_message }}
|
||||
|
||||
### Instruction:
|
||||
{{ set_response(prompt, "\n\n### Response:\n") }}
|
||||
4
templates/llm-prompt/Amethyst-20B.jinja2
Normal file
4
templates/llm-prompt/Amethyst-20B.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ system_message }}
|
||||
|
||||
### Instruction:
|
||||
{{ set_response(prompt, "\n\n### Response:\n") }}
|
||||
4
templates/llm-prompt/Amethyst.jinja2
Normal file
4
templates/llm-prompt/Amethyst.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ system_message }}
|
||||
|
||||
### Instruction:
|
||||
{{ set_response(prompt, "\n\n### Response:\n") }}
|
||||
4
templates/llm-prompt/CausalLM.jinja2
Normal file
4
templates/llm-prompt/CausalLM.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
<|im_start|>system
|
||||
{{ system_message }}<|im_end|>
|
||||
<|im_start|>user
|
||||
{{ set_response(prompt, "<|im_end|>\n<|im_start|>assistant\n") }}
|
||||
4
templates/llm-prompt/ChatLM.jinja2
Normal file
4
templates/llm-prompt/ChatLM.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
<|im_start|>system
|
||||
{{ system_message }}<|im_end|>
|
||||
<|im_start|>user
|
||||
{{ set_response(prompt, "<|im_end|>\n<|im_start|>assistant\n") }}
|
||||
4
templates/llm-prompt/OpenHermes-2-Mistral-7B.jinja2
Normal file
4
templates/llm-prompt/OpenHermes-2-Mistral-7B.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
<|im_start|>system
|
||||
{{ system_message }}<|im_end|>
|
||||
<|im_start|>user
|
||||
{{ set_response(prompt, "<|im_end|>\n<|im_start|>assistant\n") }}
|
||||
2
templates/llm-prompt/SynthIA.jinja2
Normal file
2
templates/llm-prompt/SynthIA.jinja2
Normal file
@@ -0,0 +1,2 @@
|
||||
SYSTEM: {{ system_message }}
|
||||
USER: {{ set_response(prompt, "\nASSISTANT: ") }}
|
||||
4
templates/llm-prompt/Tiefighter.jinja2
Normal file
4
templates/llm-prompt/Tiefighter.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ system_message }}
|
||||
|
||||
### Instruction:
|
||||
{{ set_response(prompt, "\n\n### Response:\n") }}
|
||||
4
templates/llm-prompt/Xwin-MLewd.jinja2
Normal file
4
templates/llm-prompt/Xwin-MLewd.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ system_message }}
|
||||
|
||||
### Instruction:
|
||||
{{ set_response(prompt, "\n\n### Response:\n") }}
|
||||
4
templates/llm-prompt/Zephyr.jinja2
Normal file
4
templates/llm-prompt/Zephyr.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
<|system|>
|
||||
{{ system_message }}</s>
|
||||
<|user|>
|
||||
{{ set_response(prompt, "</s>\n<|assistant|>\n") }}
|
||||
4
templates/llm-prompt/dolphin-2.1-mistral.jinja2
Normal file
4
templates/llm-prompt/dolphin-2.1-mistral.jinja2
Normal file
@@ -0,0 +1,4 @@
|
||||
<|im_start|>system
|
||||
{{ system_message }}<|im_end|>
|
||||
<|im_start|>user
|
||||
{{ set_response(prompt, "<|im_end|>\n<|im_start|>assistant\n") }}
|
||||
2
templates/llm-prompt/lzlv.jinja2
Normal file
2
templates/llm-prompt/lzlv.jinja2
Normal file
@@ -0,0 +1,2 @@
|
||||
SYSTEM: {{ system_message }}
|
||||
USER: {{ set_response(prompt, "\nASSISTANT: ") }}
|
||||
10
update.bat
Normal file
10
update.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
|
||||
REM activate the virtual environment
|
||||
call talemate_env\Scripts\activate
|
||||
|
||||
REM use poetry to install dependencies
|
||||
python -m poetry install
|
||||
|
||||
echo Virtual environment re-created.
|
||||
pause
|
||||
Reference in New Issue
Block a user