Compare commits

..

2 Commits

Author SHA1 Message Date
veguAI
303ec2a139 Prep 0.18.0 (#58)
* vuetify update
recent saves

* use placeholder instead of prefilling text

* fix scene loading when no coverage image is set

* improve summarize and pin response quality

* summarization use previous entries as informative context

* fixes #49: auto save indicator missleading

* regenerate with instructions

* allow resetting of state reinforcement

* creative tools: introduce new character
creative tools: introduce passive character as active character

* character creation adjustments

* no longer needed

* activate, deactivate characters (work in progress)

* worldstate manager show inactive characters

* allow setting of llm prompt template from ux
reorganize llm prompt template directory for easier local overriding
support a more sane way to write llm prompt templates

* determine prompt template from huggingface

* ignore user overrides

* fix issue with removing narrator messages

* summarization agent config for prev entry inclusion
agent config attribute notes

* client code clean up to allow modularity of clients + generic openai compatible api client

* more client cleanup

* remove debug msg, step size for ctx upped to 1024

* wip on stepped history summarization

* summarization prompt fixes

* include time message for hisory context pushed in scene.context_history

* add / remove characters toggle narration of via ctrl

* fix pydantic namespace warning
fix client emit after reconfig

* set memory ids on character detail entries

* deal with chromadb race condition (maybe)

* activate / deactivate characters from creative editor
switch creative editor to edit characters through world state manager

* set 0.18.0

* relock dependencies

* openai client shortcut to set api key if not set

* set error_action to null

* if scene has just started provide intro for extra context in is_prsent and is_leaving queries

* nice error if determine template via huggingface doesn't work

* fix issue where regenerate would sometimes pick the wrong npc if there are multiple characters talking

* add new openai models

* default to gpt-4-turbo-preview
2024-01-26 12:42:21 +02:00
vegu-ai-tools
0303a42699 formatting 2024-01-19 11:52:00 +02:00
156 changed files with 2883 additions and 782 deletions

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
talemate_env
chroma
config.yaml
templates/llm-prompt/user/*.jinja2
scenes/
!scenes/infinity-quest-dynamic-scenario/
!scenes/infinity-quest-dynamic-scenario/assets/

View File

@@ -6,7 +6,6 @@ Allows you to play roleplay scenarios with large language models.
|![Screenshot 1](docs/img/0.17.0/ss-1.png)|![Screenshot 2](docs/img/0.17.0/ss-2.png)|
|------------------------------------------|------------------------------------------|
|![Screenshot 1](docs/img/0.17.0/ss-4.png)|![Screenshot 2](docs/img/0.17.0/ss-3.png)|
|------------------------------------------|------------------------------------------|
> :warning: **It does not run any large language models itself but relies on existing APIs. Currently supports OpenAI, text-generation-webui and LMStudio.**

969
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "talemate"
version = "0.17.0"
version = "0.18.0"
description = "AI-backed roleplay and narrative tools"
authors = ["FinalWombat"]
license = "GNU Affero General Public License v3.0"
@@ -38,6 +38,7 @@ isodate = ">=0.6.1"
thefuzz = ">=0.20.0"
tiktoken = ">=0.5.1"
nltk = ">=3.8.1"
huggingface-hub = ">=0.20.2"
# ChromaDB
chromadb = ">=0.4.17,<1"

View File

@@ -2,4 +2,4 @@ from .agents import Agent
from .client import TextGeneratorWebuiClient
from .tale_mate import *
VERSION = "0.17.0"
VERSION = "0.18.0"

View File

@@ -36,6 +36,7 @@ class AgentActionConfig(pydantic.BaseModel):
step: Union[int, float, None] = None
scope: str = "global"
choices: Union[list[dict[str, str]], None] = None
note: Union[str, None] = None
class Config:
arbitrary_types_allowed = True

View File

@@ -471,12 +471,11 @@ class ConversationAgent(Agent):
set_client_context_attribute("nuke_repetition", nuke_repetition)
@set_processing
async def converse(self, actor, editor=None):
async def converse(self, actor):
"""
Have a conversation with the AI
"""
history = actor.history
self.current_memory_context = None
character = actor.character

View File

@@ -106,7 +106,19 @@ class MemoryAgent(Agent):
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, functools.partial(self._add, text, character, uid=uid, ts=ts, **kwargs))
try:
await loop.run_in_executor(None, functools.partial(self._add, text, character, uid=uid, ts=ts, **kwargs))
except AttributeError as e:
# not sure how this sometimes happens.
# chromadb model None
# race condition because we are forcing async context onto it?
log.error("memory agent", error="failed to add memory", details=e, text=text[:50], character=character, uid=uid, ts=ts, **kwargs)
await asyncio.sleep(1.0)
try:
await loop.run_in_executor(None, functools.partial(self._add, text, character, uid=uid, ts=ts, **kwargs))
except Exception as e:
log.error("memory agent", error="failed to add memory (retried)", details=e, text=text[:50], character=character, uid=uid, ts=ts, **kwargs)
def _add(self, text, character=None, ts:str=None, **kwargs):
raise NotImplementedError()

View File

@@ -453,6 +453,69 @@ class NarratorAgent(Agent):
return response
@set_processing
async def narrate_character_entry(self, character:Character, direction:str=None):
"""
Narrate a character entering the scene
"""
response = await Prompt.request(
"narrator.narrate-character-entry",
self.client,
"narrate",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"character": character,
"direction": direction,
"extra_instructions": self.extra_instructions,
}
)
response = self.clean_result(response.strip().strip("*"))
response = f"*{response}*"
return response
@set_processing
async def narrate_character_exit(self, character:Character, direction:str=None):
"""
Narrate a character exiting the scene
"""
response = await Prompt.request(
"narrator.narrate-character-exit",
self.client,
"narrate",
vars = {
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"character": character,
"direction": direction,
"extra_instructions": self.extra_instructions,
}
)
response = self.clean_result(response.strip().strip("*"))
response = f"*{response}*"
return response
async def action_to_narration(
self,
action_name: str,
*args,
**kwargs,
):
# calls self[action_name] and returns the result as a NarratorMessage
# that is pushed to the history
fn = getattr(self, action_name)
narration = await fn(*args, **kwargs)
narrator_message = NarratorMessage(narration, source=f"{action_name}:{args[0] if args else ''}".rstrip(":"))
self.scene.push_history(narrator_message)
return narrator_message
# LLM client related methods. These are called during or after the client
def inject_prompt_paramters(self, prompt_param: dict, kind: str, agent_function_name: str):
@@ -466,4 +529,5 @@ class NarratorAgent(Agent):
if auto and not self.actions["auto_break_repetition"].enabled:
return False
return True
return True

View File

@@ -63,6 +63,16 @@ class SummarizeAgent(Agent):
{"label": "Lengthy & Detailed", "value": "long"},
],
),
"include_previous": AgentActionConfig(
type="number",
label="Use preceeding summaries to strengthen context",
description="Number of entries",
note="Help the AI summarize by including the last few summaries as additional context. Some models may incorporate this context into the new summary directly, so if you find yourself with a bunch of similar history entries, try setting this to 0.",
value=3,
min=0,
max=10,
step=1,
),
}
)
}
@@ -109,6 +119,15 @@ class SummarizeAgent(Agent):
start = 0
else:
start = recent_entry.get("end", 0)+1
# if there is a recent entry we also collect the 3 most recentries
# as extra context
num_previous = self.actions["archive"].config["include_previous"].value
if recent_entry and num_previous > 0:
extra_context = "\n\n".join([entry["text"] for entry in scene.archived_history[-num_previous:]])
else:
extra_context = None
tokens = 0
dialogue_entries = []
@@ -156,10 +175,6 @@ class SummarizeAgent(Agent):
log.debug("build_archive", start=start, end=end, ts=ts, time_passage_termination=time_passage_termination)
extra_context = None
if recent_entry:
extra_context = recent_entry["text"]
# in order to summarize coherently, we need to determine if there is a favorable
# cutoff point (e.g., the scene naturally ends or shifts meaninfully in the middle
# of the dialogue)
@@ -218,6 +233,7 @@ class SummarizeAgent(Agent):
text: str,
extra_context: str = None,
method: str = None,
extra_instructions: str = None,
):
"""
Summarize the given text
@@ -228,8 +244,135 @@ class SummarizeAgent(Agent):
"scene": self.scene,
"max_tokens": self.client.max_token_length,
"summarization_method": self.actions["archive"].config["method"].value if method is None else method,
"extra_context": extra_context or "",
"extra_instructions": extra_instructions or "",
})
self.scene.log.info("summarize", dialogue_length=len(text), summarized_length=len(response))
return self.clean_result(response)
return self.clean_result(response)
async def build_stepped_archive_for_level(self, level:int):
"""
WIP - not yet used
This will iterate over existing archived_history entries
and stepped_archived_history entries and summarize based on time duration
indicated between the entries.
The lowest level of summarization (based on token threshold and any time passage)
happens in build_archive. This method is for summarizing furhter levels based on
long time pasages.
Level 0: small timestap summarize (summarizes all token summarizations when time advances +1 day)
Level 1: medium timestap summarize (summarizes all small timestep summarizations when time advances +1 week)
Level 2: large timestap summarize (summarizes all medium timestep summarizations when time advances +1 month)
Level 3: huge timestap summarize (summarizes all large timestep summarizations when time advances +1 year)
Level 4: massive timestap summarize (summarizes all huge timestep summarizations when time advances +10 years)
Level 5: epic timestap summarize (summarizes all massive timestep summarizations when time advances +100 years)
and so on (increasing by a factor of 10 each time)
```
@dataclass
class ArchiveEntry:
text: str
start: int = None
end: int = None
ts: str = None
```
Like token summarization this will use ArchiveEntry and start and end will refer to the entries in the
lower level of summarization.
Ts is the iso8601 timestamp of the start of the summarized period.
"""
# select the list to use for the entries
if level == 0:
entries = self.scene.archived_history
else:
entries = self.scene.stepped_archived_history[level-1]
# select the list to summarize new entries to
target = self.scene.stepped_archived_history[level]
if not target:
raise ValueError(f"Invalid level {level}")
# determine the start and end of the period to summarize
if not entries:
return
# determine the time threshold for this level
# first calculate all possible thresholds in iso8601 format, starting with 1 day
thresholds = [
"P1D",
"P1W",
"P1M",
"P1Y",
]
# TODO: auto extend?
time_threshold_in_seconds = util.iso8601_to_seconds(thresholds[level])
if not time_threshold_in_seconds:
raise ValueError(f"Invalid level {level}")
# determine the most recent summarized entry time, and then find entries
# that are newer than that in the lower list
ts = target[-1].ts if target else entries[0].ts
# determine the most recent entry at the lower level, if its not newer or
# the difference is less than the threshold, then we don't need to summarize
recent_entry = entries[-1]
if util.iso8601_diff(recent_entry.ts, ts) < time_threshold_in_seconds:
return
log.debug("build_stepped_archive", level=level, ts=ts)
# if target is empty, start is 0
# otherwise start is the end of the last entry
start = 0 if not target else target[-1].end
# collect entries starting at start until the combined time duration
# exceeds the threshold
entries_to_summarize = []
for entry in entries[start:]:
entries_to_summarize.append(entry)
if util.iso8601_diff(entry.ts, ts) > time_threshold_in_seconds:
break
# summarize the entries
# we also collect N entries of previous summaries to use as context
num_previous = self.actions["archive"].config["include_previous"].value
if num_previous > 0:
extra_context = "\n\n".join([entry["text"] for entry in target[-num_previous:]])
else:
extra_context = None
summarized = await self.summarize(
"\n".join(map(str, entries_to_summarize)), extra_context=extra_context
)
# push summarized entry to target
ts = entries_to_summarize[-1].ts
target.append(data_objects.ArchiveEntry(summarized, start, len(entries_to_summarize)-1, ts=ts))

View File

@@ -439,7 +439,7 @@ class WorldStateAgent(Agent):
@set_processing
async def update_reinforcement(self, question:str, character:str=None):
async def update_reinforcement(self, question:str, character:str=None, reset:bool=False):
"""
Queries a single re-inforcement
@@ -450,6 +450,11 @@ class WorldStateAgent(Agent):
if not reinforcement:
return
source = f"{reinforcement.question}:{reinforcement.character if reinforcement.character else ''}"
if reset and reinforcement.insert == "sequential":
self.scene.pop_history(typ="reinforcement", source=source, all=True)
answer = await Prompt.request(
"world_state.update-reinforcements",
self.client,
@@ -460,24 +465,23 @@ class WorldStateAgent(Agent):
"question": reinforcement.question,
"instructions": reinforcement.instructions or "",
"character": self.scene.get_character(reinforcement.character) if reinforcement.character else None,
"answer": reinforcement.answer or "",
"answer": (reinforcement.answer if not reset else None) or "",
"reinforcement": reinforcement,
}
)
reinforcement.answer = answer
reinforcement.due = reinforcement.interval
source = f"{reinforcement.question}:{reinforcement.character if reinforcement.character else ''}"
# remove any recent previous reinforcement message with same question
# to avoid overloading the near history with reinforcement messages
self.scene.pop_history(typ="reinforcement", source=source, max_iterations=10)
if not reset:
self.scene.pop_history(typ="reinforcement", source=source, max_iterations=10)
if reinforcement.insert == "sequential":
# insert the reinforcement message at the current position
message = ReinforcementMessage(message=answer, source=source)
log.debug("update_reinforcement", message=message)
log.debug("update_reinforcement", message=message, reset=reset)
self.scene.push_history(message)
# if reinforcement has a character name set, update the character detail
@@ -576,7 +580,14 @@ class WorldStateAgent(Agent):
text = self.scene.snapshot(lines=num_messages, start=message_index)
summary = await summarizer.summarize(text, method="short")
extra_context = self.scene.snapshot(lines=50, start=message_index-num_messages)
summary = await summarizer.summarize(
text,
extra_context=extra_context,
method="short",
extra_instructions="Pay particularly close attention to decisions, agreements or promises made.",
)
entry_id = util.clean_id(await creator.generate_title(summary))
@@ -606,4 +617,48 @@ class WorldStateAgent(Agent):
)
await self.scene.load_active_pins()
self.scene.emit_status()
self.scene.emit_status()
@set_processing
async def is_character_present(self, character:str) -> bool:
"""
Check if a character is present in the scene
Arguments:
- `character`: The character to check.
"""
if len(self.scene.history) < 10:
text = self.scene.intro+"\n\n"+self.scene.snapshot(lines=50)
else:
text = self.scene.snapshot(lines=50)
is_present = await self.analyze_text_and_answer_question(
text=text,
query=f"Is {character} present AND active in the current scene? Answert with 'yes' or 'no'.",
)
return is_present.lower().startswith("y")
@set_processing
async def is_character_leaving(self, character:str) -> bool:
"""
Check if a character is leaving the scene
Arguments:
- `character`: The character to check.
"""
if len(self.scene.history) < 10:
text = self.scene.intro+"\n\n"+self.scene.snapshot(lines=50)
else:
text = self.scene.snapshot(lines=50)
is_leaving = await self.analyze_text_and_answer_question(
text=text,
query=f"Is {character} leaving the current scene? Answert with 'yes' or 'no'.",
)
return is_leaving.lower().startswith("y")

57
src/talemate/character.py Normal file
View File

@@ -0,0 +1,57 @@
from typing import Union, TYPE_CHECKING
from talemate.instance import get_agent
if TYPE_CHECKING:
from talemate.tale_mate import Scene, Character, Actor
__all__ = [
"deactivate_character",
"activate_character",
]
async def deactivate_character(scene:"Scene", character:Union[str, "Character"]):
"""
Deactivates a character
Arguments:
- `scene`: The scene to deactivate the character from
- `character`: The character to deactivate. Can be a string (the character's name) or a Character object
"""
if isinstance(character, str):
character = scene.get_character(character)
if character.is_player:
# can't deactivate the player
return False
if character.name in scene.inactive_characters:
# already deactivated
return False
await scene.remove_actor(character.actor)
scene.inactive_characters[character.name] = character
async def activate_character(scene:"Scene", character:Union[str, "Character"]):
"""
Activates a character
Arguments:
- `scene`: The scene to activate the character in
- `character`: The character to activate. Can be a string (the character's name) or a Character object
"""
if isinstance(character, str):
character = scene.get_character(character)
if character.name not in scene.inactive_characters:
# already activated
return False
actor = scene.Actor(character, get_agent("conversation"))
await scene.add_actor(actor)
del scene.inactive_characters[character.name]

View File

@@ -3,4 +3,5 @@ from talemate.client.openai import OpenAIClient
from talemate.client.registry import CLIENT_CLASSES, get_client_class, register
from talemate.client.textgenwebui import TextGeneratorWebuiClient
from talemate.client.lmstudio import LMStudioClient
from talemate.client.openai_compat import OpenAICompatibleClient
import talemate.client.runpod

View File

@@ -1,14 +1,14 @@
"""
A unified client base, based on the openai API
"""
import copy
import random
import time
from typing import Callable
import pydantic
from typing import Callable, Union
import structlog
import logging
from openai import AsyncOpenAI
from openai import AsyncOpenAI, PermissionDeniedError
from talemate.emit import emit
import talemate.instance as instance
@@ -29,10 +29,21 @@ REMOTE_SERVICES = [
STOPPING_STRINGS = ["<|im_end|>", "</s>"]
class ErrorAction(pydantic.BaseModel):
title:str
action_name:str
icon:str = "mdi-error"
arguments:list = []
class Defaults(pydantic.BaseModel):
api_url:str = "http://localhost:5000"
max_token_length:int = 4096
class ClientBase:
api_url: str
model_name: str
api_key: str = None
name:str = None
enabled: bool = True
current_status: str = None
@@ -43,8 +54,15 @@ class ClientBase:
auto_break_repetition_enabled: bool = True
client_type = "base"
class Meta(pydantic.BaseModel):
experimental:Union[None,str] = None
defaults:Defaults = Defaults()
title:str = "Client"
name_prefix:str = "Client"
enable_api_auth: bool = False
requires_prompt_template: bool = True
def __init__(
self,
api_url: str = None,
@@ -60,6 +78,10 @@ class ClientBase:
def __str__(self):
return f"{self.client_type}Client[{self.api_url}][{self.model_name or ''}]"
@property
def experimental(self):
return False
def set_client(self, **kwargs):
self.client = AsyncOpenAI(base_url=self.api_url, api_key="sk-1111")
@@ -73,20 +95,15 @@ class ClientBase:
if not self.model_name:
self.log.warning("prompt template not applied", reason="no model loaded")
return f"{sys_msg}\n{prompt}"
return model_prompt(self.model_name, sys_msg, prompt)
def has_prompt_template(self):
if not self.model_name:
return False
return model_prompt(self.model_name, sys_msg, prompt)[0]
return model_prompt.exists(self.model_name)
def prompt_template_example(self):
if not self.model_name:
return None
if not getattr(self, "model_name", None):
return None, None
return model_prompt(self.model_name, "sysmsg", "prompt<|BOT|>{LLM coercion}")
def reconfigure(self, **kwargs):
"""
@@ -192,6 +209,8 @@ class ClientBase:
status_change = status != self.current_status
self.current_status = status
prompt_template_example, prompt_template_file = self.prompt_template_example()
emit(
"client_status",
message=self.client_type,
@@ -199,8 +218,12 @@ class ClientBase:
details=model_name,
status=status,
data={
"prompt_template_example": self.prompt_template_example(),
"has_prompt_template": self.has_prompt_template(),
"api_key": self.api_key,
"prompt_template_example": prompt_template_example,
"has_prompt_template": (prompt_template_file and prompt_template_file != "default.jinja2"),
"template_file": prompt_template_file,
"meta": self.Meta().model_dump(),
"error_action": None,
}
)
@@ -295,8 +318,13 @@ class ClientBase:
try:
response = await self.client.completions.create(prompt=prompt.strip(" "), **parameters)
return response.get("choices", [{}])[0].get("text", "")
except PermissionDeniedError as e:
self.log.error("generate error", e=e)
emit("status", message="Client API: Permission Denied", status="error")
return ""
except Exception as e:
self.log.error("generate error", e=e)
emit("status", message="Error during generation (check logs)", status="error")
return ""
async def send_prompt(

View File

@@ -1,14 +1,23 @@
import pydantic
from talemate.client.base import ClientBase
from talemate.client.registry import register
from openai import AsyncOpenAI
class Defaults(pydantic.BaseModel):
api_url:str = "http://localhost:1234"
@register()
class LMStudioClient(ClientBase):
client_type = "lmstudio"
conversation_retries = 5
class Meta(ClientBase.Meta):
name_prefix:str = "LMStudio"
title:str = "LMStudio"
defaults:Defaults = Defaults()
def set_client(self, **kwargs):
self.client = AsyncOpenAI(base_url=self.api_url+"/v1", api_key="sk-1111")

View File

@@ -1,6 +1,9 @@
from jinja2 import Environment, FileSystemLoader
import os
import structlog
import shutil
import huggingface_hub
import tempfile
__all__ = ["model_prompt"]
@@ -8,6 +11,21 @@ BASE_TEMPLATE_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "templates", "llm-prompt"
)
# holds the default templates
STD_TEMPLATE_PATH = os.path.join(BASE_TEMPLATE_PATH, "std")
# llm prompt templates provided by talemate
TALEMATE_TEMPLATE_PATH = os.path.join(BASE_TEMPLATE_PATH, "talemate")
# user overrides
USER_TEMPLATE_PATH = os.path.join(BASE_TEMPLATE_PATH, "user")
TEMPLATE_IDENTIFIERS = []
def register_template_identifier(cls):
TEMPLATE_IDENTIFIERS.append(cls)
return cls
log = structlog.get_logger("talemate.model_prompts")
class ModelPrompt:
@@ -24,23 +42,37 @@ class ModelPrompt:
def env(self):
if not hasattr(self, "_env"):
log.info("modal prompt", base_template_path=BASE_TEMPLATE_PATH)
self._env = Environment(loader=FileSystemLoader(BASE_TEMPLATE_PATH))
self._env = Environment(loader=FileSystemLoader([
USER_TEMPLATE_PATH,
TALEMATE_TEMPLATE_PATH,
]))
return self._env
@property
def std_templates(self) -> list[str]:
env = Environment(loader=FileSystemLoader(STD_TEMPLATE_PATH))
return sorted(env.list_templates())
def __call__(self, model_name:str, system_message:str, prompt:str):
template = self.get_template(model_name)
template, template_file = self.get_template(model_name)
if not template:
template = self.env.get_template("default.jinja2")
template_file = "default.jinja2"
template = self.env.get_template(template_file)
if "<|BOT|>" in prompt:
user_message, coercion_message = prompt.split("<|BOT|>", 1)
else:
user_message = prompt
coercion_message = ""
return template.render({
"system_message": system_message,
"prompt": prompt,
"user_message": user_message,
"coercion_message": coercion_message,
"set_response" : self.set_response
})
def exists(self, model_name:str):
return bool(self.get_template(model_name))
}), template_file
def set_response(self, prompt:str, response_str:str):
@@ -74,13 +106,200 @@ class ModelPrompt:
# If there are no matches, return None
if not matches:
return None
return None, None
# If there is only one match, return it
if len(matches) == 1:
return self.env.get_template(matches[0])
return self.env.get_template(matches[0]), matches[0]
# If there are multiple matches, return the one with the longest name
return self.env.get_template(sorted(matches, key=lambda x: len(x), reverse=True)[0])
sorted_matches = sorted(matches, key=lambda x: len(x), reverse=True)
return self.env.get_template(sorted_matches[0]), sorted_matches[0]
model_prompt = ModelPrompt()
def create_user_override(self, template_name:str, model_name:str):
"""
Will copy STD_TEMPLATE_PATH/template_name to USER_TEMPLATE_PATH/model_name.jinja2
"""
template_name = template_name.split(".jinja2")[0]
shutil.copyfile(
os.path.join(STD_TEMPLATE_PATH, template_name + ".jinja2"),
os.path.join(USER_TEMPLATE_PATH, model_name + ".jinja2")
)
return os.path.join(USER_TEMPLATE_PATH, model_name + ".jinja2")
def query_hf_for_prompt_template_suggestion(self, model_name:str):
print("query_hf_for_prompt_template_suggestion", model_name)
api = huggingface_hub.HfApi()
try:
author, model_name = model_name.split("_", 1)
except ValueError:
return None
models = list(api.list_models(
filter=huggingface_hub.ModelFilter(model_name=model_name, author=author)
))
if not models:
return None
model = models[0]
repo_id = f"{author}/{model_name}"
with tempfile.TemporaryDirectory() as tmpdir:
readme_path = huggingface_hub.hf_hub_download(repo_id=repo_id, filename="README.md", cache_dir=tmpdir)
if not readme_path:
return None
with open(readme_path) as f:
readme = f.read()
for identifer_cls in TEMPLATE_IDENTIFIERS:
identifier = identifer_cls()
if identifier(readme):
return f"{identifier.template_str}.jinja2"
model_prompt = ModelPrompt()
class TemplateIdentifier:
def __call__(self, content:str):
return False
@register_template_identifier
class Llama2Identifier(TemplateIdentifier):
template_str = "Llama2"
def __call__(self, content:str):
return "[INST]" in content and "[/INST]" in content
@register_template_identifier
class ChatMLIdentifier(TemplateIdentifier):
template_str = "ChatML"
def __call__(self, content:str):
"""
<|im_start|>system
{{ system_message }}<|im_end|>
<|im_start|>user
{{ user_message }}<|im_end|>
<|im_start|>assistant
{{ coercion_message }}
"""
return (
"<|im_start|>system" in content
and "<|im_end|>" in content
and "<|im_start|>user" in content
and "<|im_start|>assistant" in content
)
@register_template_identifier
class InstructionInputResponseIdentifier(TemplateIdentifier):
template_str = "InstructionInputResponse"
def __call__(self, content:str):
return (
"### Instruction:" in content
and "### Input:" in content
and "### Response:" in content
)
@register_template_identifier
class AlpacaIdentifier(TemplateIdentifier):
template_str = "Alpaca"
def __call__(self, content:str):
"""
{{ system_message }}
### Instruction:
{{ user_message }}
### Response:
{{ coercion_message }}
"""
return (
"### Instruction:" in content
and "### Response:" in content
)
@register_template_identifier
class OpenChatIdentifier(TemplateIdentifier):
template_str = "OpenChat"
def __call__(self, content:str):
"""
GPT4 Correct System: {{ system_message }}<|end_of_turn|>GPT4 Correct User: {{ user_message }}<|end_of_turn|>GPT4 Correct Assistant: {{ coercion_message }}
"""
return (
"<|end_of_turn|>" in content
and "GPT4 Correct System:" in content
and "GPT4 Correct User:" in content
and "GPT4 Correct Assistant:" in content
)
@register_template_identifier
class VicunaIdentifier(TemplateIdentifier):
template_str = "Vicuna"
def __call__(self, content:str):
"""
SYSTEM: {{ system_message }}
USER: {{ user_message }}
ASSISTANT: {{ coercion_message }}
"""
return (
"SYSTEM:" in content
and "USER:" in content
and "ASSISTANT:" in content
)
@register_template_identifier
class USER_ASSISTANTIdentifier(TemplateIdentifier):
template_str = "USER_ASSISTANT"
def __call__(self, content:str):
"""
USER: {{ system_message }} {{ user_message }} ASSISTANT: {{ coercion_message }}
"""
return (
"USER:" in content
and "ASSISTANT:" in content
)
@register_template_identifier
class UserAssistantIdentifier(TemplateIdentifier):
template_str = "UserAssistant"
def __call__(self, content:str):
"""
User: {{ system_message }} {{ user_message }}
Assistant: {{ coercion_message }}
"""
return (
"User:" in content
and "Assistant:" in content
)
@register_template_identifier
class ZephyrIdentifier(TemplateIdentifier):
template_str = "Zephyr"
def __call__(self, content:str):
"""
<|system|>
{{ system_message }}</s>
<|user|>
{{ user_message }}</s>
<|assistant|>
{{ coercion_message }}
"""
return (
"<|system|>" in content
and "<|user|>" in content
and "<|assistant|>" in content
)

View File

@@ -1,17 +1,12 @@
import os
import json
import traceback
from openai import AsyncOpenAI
import pydantic
from openai import AsyncOpenAI, PermissionDeniedError
from talemate.client.base import ClientBase
from talemate.client.base import ClientBase, ErrorAction
from talemate.client.registry import register
from talemate.emit import emit
from talemate.emit.signals import handlers
import talemate.emit.async_signals as async_signals
from talemate.config import load_config
import talemate.instance as instance
import talemate.client.system_prompts as system_prompts
import structlog
import tiktoken
@@ -71,6 +66,10 @@ def num_tokens_from_messages(messages:list[dict], model:str="gpt-3.5-turbo-0613"
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
class Defaults(pydantic.BaseModel):
max_token_length:int = 16384
model:str = "gpt-4-turbo-preview"
@register()
class OpenAIClient(ClientBase):
"""
@@ -80,8 +79,23 @@ class OpenAIClient(ClientBase):
client_type = "openai"
conversation_retries = 0
auto_break_repetition_enabled = False
class Meta(ClientBase.Meta):
name_prefix:str = "OpenAI"
title:str = "OpenAI"
manual_model:bool = True
manual_model_choices:list[str] = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-1106-preview",
"gpt-4-0125-preview",
"gpt-4-turbo-preview",
]
requires_prompt_template: bool = False
defaults:Defaults = Defaults()
def __init__(self, model="gpt-4-1106-preview", **kwargs):
def __init__(self, model="gpt-4-turbo-preview", **kwargs):
self.model_name = model
self.api_key_status = None
@@ -96,6 +110,7 @@ class OpenAIClient(ClientBase):
def emit_status(self, processing: bool = None):
error_action = None
if processing is not None:
self.processing = processing
@@ -105,6 +120,15 @@ class OpenAIClient(ClientBase):
else:
status = "error"
model_name = "No API key set"
error_action = ErrorAction(
title="Set API Key",
action_name="openAppConfig",
icon="mdi-key-variant",
arguments=[
"application",
"openai_api",
]
)
if not self.model_name:
status = "error"
@@ -118,6 +142,10 @@ class OpenAIClient(ClientBase):
id=self.name,
details=model_name,
status=status,
data={
"error_action": error_action.model_dump() if error_action else None,
"meta": self.Meta().model_dump(),
}
)
def set_client(self, max_token_length:int=None):
@@ -208,8 +236,8 @@ class OpenAIClient(ClientBase):
if not self.openai_api_key:
raise Exception("No OpenAI API key set")
# only gpt-4-1106-preview supports json_object response coersion
supports_json_object = self.model_name in ["gpt-4-1106-preview"]
# only gpt-4-* supports enforcing json object
supports_json_object = self.model_name.startswith("gpt-4-")
right = None
try:
_, right = prompt.split("\nContinue this response: ")
@@ -235,6 +263,9 @@ class OpenAIClient(ClientBase):
response = response[len(right):].strip()
return response
except PermissionDeniedError as e:
self.log.error("generate error", e=e)
emit("status", message="OpenAI API: Permission Denied", status="error")
return ""
except Exception as e:
raise

View File

@@ -0,0 +1,111 @@
import pydantic
from talemate.client.base import ClientBase
from talemate.client.registry import register
from openai import AsyncOpenAI, PermissionDeniedError, NotFoundError
from talemate.emit import emit
EXPERIMENTAL_DESCRIPTION = """Use this client if you want to connect to a service implementing an OpenAI-compatible API. Success is going to depend on the level of compatibility. Use the actual OpenAI client if you want to connect to OpenAI's API."""
class Defaults(pydantic.BaseModel):
api_url:str = "http://localhost:5000"
api_key:str = ""
max_token_length:int = 4096
model:str = ""
@register()
class OpenAICompatibleClient(ClientBase):
client_type = "openai_compat"
conversation_retries = 5
class Meta(ClientBase.Meta):
title:str = "OpenAI Compatible API"
name_prefix:str = "OpenAI Compatible API"
experimental:str = EXPERIMENTAL_DESCRIPTION
enable_api_auth:bool = True
manual_model:bool = True
defaults:Defaults = Defaults()
def __init__(self, model=None, **kwargs):
self.model_name = model
super().__init__(**kwargs)
@property
def experimental(self):
return EXPERIMENTAL_DESCRIPTION
def set_client(self, **kwargs):
self.api_key = kwargs.get("api_key")
self.client = AsyncOpenAI(base_url=self.api_url+"/v1", api_key=self.api_key)
self.model_name = kwargs.get("model") or kwargs.get("model_name") or self.model_name
def tune_prompt_parameters(self, parameters:dict, kind:str):
super().tune_prompt_parameters(parameters, kind)
keys = list(parameters.keys())
valid_keys = ["temperature", "top_p"]
for key in keys:
if key not in valid_keys:
del parameters[key]
async def get_model_name(self):
try:
model_name = await super().get_model_name()
except NotFoundError as e:
# api does not implement model listing
return self.model_name
except Exception as e:
self.log.error("get_model_name error", e=e)
return self.model_name
# model name may be a file path, so we need to extract the model name
# the path could be windows or linux so it needs to handle both backslash and forward slash
is_filepath = "/" in model_name
is_filepath_windows = "\\" in model_name
if is_filepath or is_filepath_windows:
model_name = model_name.replace("\\", "/").split("/")[-1]
return model_name
async def generate(self, prompt:str, parameters:dict, kind:str):
"""
Generates text from the given prompt and parameters.
"""
human_message = {'role': 'user', 'content': prompt.strip()}
self.log.debug("generate", prompt=prompt[:128]+" ...", parameters=parameters)
try:
response = await self.client.chat.completions.create(
model=self.model_name, messages=[human_message], **parameters
)
return response.choices[0].message.content
except PermissionDeniedError as e:
self.log.error("generate error", e=e)
emit("status", message="Client API: Permission Denied", status="error")
return ""
except Exception as e:
self.log.error("generate error", e=e)
emit("status", message="Error during generation (check logs)", status="error")
return ""
def reconfigure(self, **kwargs):
if kwargs.get("model"):
self.model_name = kwargs["model"]
if "api_url" in kwargs:
self.api_url = kwargs["api_url"]
if "max_token_length" in kwargs:
self.max_token_length = kwargs["max_token_length"]
if "api_key" in kwargs:
self.api_auth = kwargs["api_key"]
self.set_client(**kwargs)

View File

@@ -2,7 +2,6 @@ from talemate.client.base import ClientBase, STOPPING_STRINGS
from talemate.client.registry import register
from openai import AsyncOpenAI
import httpx
import copy
import random
import structlog
@@ -13,6 +12,10 @@ class TextGeneratorWebuiClient(ClientBase):
client_type = "textgenwebui"
class Meta(ClientBase.Meta):
name_prefix:str = "TextGenWebUI"
title:str = "Text-Generation-WebUI (ooba)"
def tune_prompt_parameters(self, parameters:dict, kind:str):
super().tune_prompt_parameters(parameters, kind)
parameters["stopping_strings"] = STOPPING_STRINGS + parameters.get("extra_stopping_strings", [])

View File

@@ -1,4 +1,5 @@
from .base import TalemateCommand
from .cmd_characters import *
from .cmd_debug_tools import *
from .cmd_dialogue import *
from .cmd_director import CmdDirectorDirect, CmdDirectorDirectWithOverride
@@ -12,7 +13,7 @@ from .cmd_memset import CmdMemset
from .cmd_narrate import *
from .cmd_rebuild_archive import CmdRebuildArchive
from .cmd_rename import CmdRename
from .cmd_rerun import CmdRerun
from .cmd_rerun import *
from .cmd_reset import CmdReset
from .cmd_rm import CmdRm
from .cmd_remove_character import CmdRemoveCharacter

View File

@@ -0,0 +1,142 @@
import structlog
from talemate.commands.base import TalemateCommand
from talemate.commands.manager import register
from talemate.emit import wait_for_input, emit
from talemate.character import deactivate_character, activate_character
from talemate.instance import get_agent
log = structlog.get_logger("talemate.cmd.characters")
__all__ = [
"CmdDeactivateCharacter",
"CmdActivateCharacter",
]
@register
class CmdDeactivateCharacter(TalemateCommand):
"""
Deactivates a character
"""
name = "character_deactivate"
description = "Will deactivate a character"
aliases = ["char_d"]
label = "Character exit"
async def run(self):
narrator = get_agent("narrator")
world_state = get_agent("world_state")
characters = list([character.name for character in self.scene.get_npc_characters()])
if not characters:
emit("status", message="No characters found", status="error")
return True
if self.args:
character_name = self.args[0]
else:
character_name = await wait_for_input("Which character do you want to deactivate?", data={
"input_type": "select",
"choices": characters,
})
if not character_name:
emit("status", message="No character selected", status="error")
return True
never_narrate = len(self.args) > 1 and self.args[1] == "no"
if not never_narrate:
is_present = await world_state.is_character_present(character_name)
is_leaving = await world_state.is_character_leaving(character_name)
log.debug("deactivate_character", character_name=character_name, is_present=is_present, is_leaving=is_leaving, never_narrate=never_narrate)
else:
is_present = False
is_leaving = True
log.debug("deactivate_character", character_name=character_name, never_narrate=never_narrate)
if is_present and not is_leaving and not never_narrate:
direction = await wait_for_input(f"How does {character_name} exit the scene? (leave blank for AI to decide)")
message = await narrator.action_to_narration(
"narrate_character_exit",
self.scene.get_character(character_name),
direction = direction,
)
self.narrator_message(message)
await deactivate_character(self.scene, character_name)
emit("status", message=f"Deactivated {character_name}", status="success")
self.scene.emit_status()
self.scene.world_state.emit()
return True
@register
class CmdActivateCharacter(TalemateCommand):
"""
Activates a character
"""
name = "character_activate"
description = "Will activate a character"
aliases = ["char_a"]
label = "Character enter"
async def run(self):
world_state = get_agent("world_state")
narrator = get_agent("narrator")
characters = list(self.scene.inactive_characters.keys())
if not characters:
emit("status", message="No characters found", status="error")
return True
if self.args:
character_name = self.args[0]
if character_name not in characters:
emit("status", message="Character not found", status="error")
return True
else:
character_name = await wait_for_input("Which character do you want to activate?", data={
"input_type": "select",
"choices": characters,
})
if not character_name:
emit("status", message="No character selected", status="error")
return True
never_narrate = len(self.args) > 1 and self.args[1] == "no"
if not never_narrate:
is_present = await world_state.is_character_present(character_name)
log.debug("activate_character", character_name=character_name, is_present=is_present, never_narrate=never_narrate)
else:
is_present = True
log.debug("activate_character", character_name=character_name, never_narrate=never_narrate)
await activate_character(self.scene, character_name)
if not is_present and not never_narrate:
direction = await wait_for_input(f"How does {character_name} enter the scene? (leave blank for AI to decide)")
message = await narrator.action_to_narration(
"narrate_character_entry",
self.scene.get_character(character_name),
direction = direction,
)
self.narrator_message(message)
emit("status", message=f"Activated {character_name}", status="success")
self.scene.emit_status()
self.scene.world_state.emit()
return True

View File

@@ -1,6 +1,14 @@
from talemate.commands.base import TalemateCommand
from talemate.commands.manager import register
from talemate.client.context import ClientContext
from talemate.context import RerunContext
from talemate.emit import wait_for_input
__all__ = [
"CmdRerun",
"CmdRerunWithDirection",
]
@register
class CmdRerun(TalemateCommand):
@@ -15,4 +23,37 @@ class CmdRerun(TalemateCommand):
async def run(self):
nuke_repetition = self.args[0] if self.args else 0.0
with ClientContext(nuke_repetition=nuke_repetition):
await self.scene.rerun()
await self.scene.rerun()
@register
class CmdRerunWithDirection(TalemateCommand):
"""
Command class for the 'rerun_directed' command
"""
name = "rerun_directed"
description = "Rerun the scene with a direction"
aliases = ["rrd"]
label = "Directed Rerun"
async def run(self):
nuke_repetition = self.args[0] if self.args else 0.0
method = self.args[1] if len(self.args) > 1 else "replace"
if method not in ["replace", "edit"]:
raise ValueError(f"Unknown method: {method}. Valid methods are 'replace' and 'edit'.")
if method == "replace":
hint = ""
else:
hint = " (subtle change to previous generation)"
direction = await wait_for_input(f"Instructions for regeneration{hint}: ")
with RerunContext(self.scene, direction=direction, method=method):
with ClientContext(direction=direction, nuke_repetition=nuke_repetition):
await self.scene.rerun()

View File

@@ -4,6 +4,8 @@ from talemate.commands.base import TalemateCommand
from talemate.commands.manager import register
from talemate.exceptions import RestartSceneLoop
from talemate.emit import emit
@register
class CmdSetEnvironmentToScene(TalemateCommand):
@@ -26,7 +28,7 @@ class CmdSetEnvironmentToScene(TalemateCommand):
self.scene.set_environment("scene")
self.system_message(f"Game mode")
emit("status", message="Switched to gameplay", status="info")
raise RestartSceneLoop()

View File

@@ -2,10 +2,12 @@ import random
import structlog
from talemate.commands.base import TalemateCommand
from talemate.scene_message import NarratorMessage
from talemate.commands.manager import register
from talemate.emit import wait_for_input, emit
from talemate.instance import get_agent
import talemate.instance as instance
from talemate.status import set_loading, LoadingStatus
log = structlog.get_logger("talemate.cmd.world_state")
@@ -58,12 +60,16 @@ class CmdPersistCharacter(TalemateCommand):
description = "Persist a character by name"
aliases = ["pc"]
@set_loading("Generating character...", set_busy=False)
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")
narrator = instance.get_agent("narrator")
loading_status = LoadingStatus(3)
if not len(self.args):
characters = await world_state.identify_characters()
@@ -80,16 +86,35 @@ class CmdPersistCharacter(TalemateCommand):
else:
name = self.args[0]
scene.log.debug("persist_character", name=name)
extra_instructions = None
if name == "prompt":
name = await wait_for_input("What is the name of the character?")
description = await wait_for_input(f"Brief description for {name} (or leave blank):")
if description.strip():
extra_instructions = f"Name: {name}\nBrief Description: {description}"
never_narrate = len(self.args) > 1 and self.args[1] == "no"
if not never_narrate:
is_present = await world_state.is_character_present(name)
log.debug("persist_character", name=name, is_present=is_present, never_narrate=never_narrate)
else:
is_present = False
log.debug("persist_character", name=name, never_narrate=never_narrate)
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)
loading_status("Generating character attributes...")
attributes = await world_state.extract_character_sheet(name=name, text=extra_instructions)
scene.log.debug("persist_character", attributes=attributes)
character.base_attributes = attributes
loading_status("Generating character description...")
description = await creator.determine_character_description(character)
character.description = description
@@ -100,9 +125,18 @@ class CmdPersistCharacter(TalemateCommand):
await scene.add_actor(actor)
self.emit("system", f"Added character {name} to the scene.")
emit("status", message=f"Added character {name} to the scene.", status="success")
# write narrative for the character entering the scene
if not is_present and not never_narrate:
loading_status("Narrating character entrance...")
entry_narration = await narrator.narrate_character_entry(character, direction=extra_instructions)
message = NarratorMessage(entry_narration, source=f"narrate_character_entry:{character.name}")
self.narrator_message(message)
self.scene.push_history(message)
scene.emit_status()
scene.world_state.emit()
@register
class CmdAddReinforcement(TalemateCommand):
@@ -275,6 +309,6 @@ class CmdSummarizeAndPin(TalemateCommand):
raise ValueError("No history to summarize.")
message_id = int(self.args[0]) if len(self.args) else scene.history[-1].id
num_messages = int(self.args[1]) if len(self.args) > 1 else 3
num_messages = int(self.args[1]) if len(self.args) > 1 else 5
await world_state.summarize_and_pin(message_id, num_messages=num_messages)

View File

@@ -2,11 +2,16 @@ import yaml
import pydantic
import structlog
import os
import datetime
from pydantic import BaseModel, Field
from typing import Optional, Dict, Union, ClassVar
from typing import Optional, Dict, Union, ClassVar, TYPE_CHECKING
from talemate.emit import emit
from talemate.scene_assets import Asset
if TYPE_CHECKING:
from talemate.tale_mate import Scene
log = structlog.get_logger("talemate.config")
@@ -15,6 +20,7 @@ class Client(BaseModel):
name: str
model: Union[str,None] = None
api_url: Union[str,None] = None
api_key: Union[str,None] = None
max_token_length: Union[int,None] = None
class Config:
@@ -113,6 +119,52 @@ class ChromaDB(BaseModel):
instructor_model: str="default"
embeddings: str="default"
class RecentScene(BaseModel):
name: str
path: str
filename: str
date: str
cover_image: Union[Asset, None] = None
class RecentScenes(BaseModel):
scenes: list[RecentScene] = pydantic.Field(default_factory=list)
max_entries: int = 10
def push(self, scene:"Scene"):
"""
adds a scene to the recent scenes list
"""
# if scene has not been saved, don't add it
if not scene.full_path:
return
now = datetime.datetime.now()
# remove any existing entries for this scene
self.scenes = [s for s in self.scenes if s.path != scene.full_path]
# add the new entry
self.scenes.insert(0,
RecentScene(
name=scene.name,
path=scene.full_path,
filename=scene.filename,
date=now.isoformat(),
cover_image=scene.assets.assets[scene.assets.cover_image] if scene.assets.cover_image else None
))
# trim the list to max_entries
self.scenes = self.scenes[:self.max_entries]
def clean(self):
"""
removes any entries that no longer exist
"""
self.scenes = [s for s in self.scenes if os.path.exists(s.path)]
class Config(BaseModel):
clients: Dict[str, Client] = {}
game: Game
@@ -133,8 +185,13 @@ class Config(BaseModel):
tts: TTSConfig = TTSConfig()
recent_scenes: RecentScenes = RecentScenes()
class Config:
extra = "ignore"
def save(self, file_path: str = "./config.yaml"):
save_config(self, file_path)
class SceneConfig(BaseModel):
automated_actions: dict[str, bool]
@@ -145,7 +202,7 @@ class SceneAssetUpload(BaseModel):
content:str = None
def load_config(file_path: str = "./config.yaml") -> dict:
def load_config(file_path: str = "./config.yaml", as_model:bool=False) -> Union[dict, Config]:
"""
Load the config file from the given path.
@@ -158,12 +215,15 @@ def load_config(file_path: str = "./config.yaml") -> dict:
try:
config = Config(**config_data)
config.recent_scenes.clean()
except pydantic.ValidationError as e:
log.error("config validation", error=e)
return None
return config.model_dump()
if as_model:
return config
return config.model_dump()
def save_config(config, file_path: str = "./config.yaml"):
"""

View File

@@ -1,11 +1,17 @@
from contextvars import ContextVar
import structlog
__all__ = [
"scene_is_loading",
"rerun_context",
"SceneIsLoading",
"RerunContext",
]
log = structlog.get_logger(__name__)
scene_is_loading = ContextVar("scene_is_loading", default=None)
rerun_context = ContextVar("rerun_context", default=None)
class SceneIsLoading:
@@ -17,4 +23,19 @@ class SceneIsLoading:
def __exit__(self, *args):
scene_is_loading.reset(self.token)
class RerunContext:
def __init__(self, scene, direction=None, method="replace", message:str = None):
self.scene = scene
self.direction = direction
self.method = method
self.message = message
log.debug("RerunContext", scene=scene, direction=direction, method=method, message=message)
def __enter__(self):
self.token = rerun_context.set(self)
def __exit__(self, *args):
rerun_context.reset(self.token)

View File

@@ -118,6 +118,13 @@ async def emit_client_bootstraps():
data=list(await bootstrap.list_all())
)
def sync_emit_clients_status():
"""
Will emit status of all clients
in synchronous mode
"""
loop = asyncio.get_event_loop()
loop.run_until_complete(emit_clients_status())
async def sync_client_bootstraps():
"""

View File

@@ -13,6 +13,7 @@ from talemate.world_state import WorldState
from talemate.game_state import GameState
from talemate.context import SceneIsLoading
from talemate.emit import emit
from talemate.status import set_loading, LoadingStatus
import talemate.instance as instance
import structlog
@@ -28,55 +29,32 @@ __all__ = [
log = structlog.get_logger("talemate.load")
class set_loading:
def __init__(self, message):
self.message = message
def __call__(self, fn):
async def wrapper(*args, **kwargs):
emit("status", message=self.message, status="busy")
try:
return await fn(*args, **kwargs)
finally:
emit("status", message="", status="idle")
return wrapper
class LoadingStatus:
def __init__(self, max_steps:int):
self.max_steps = max_steps
self.current_step = 0
def __call__(self, message:str):
self.current_step += 1
emit("status", message=f"{message} [{self.current_step}/{self.max_steps}]", status="busy")
@set_loading("Loading scene...")
async def load_scene(scene, file_path, conv_client, reset: bool = False):
"""
Load the scene data from the given file path.
"""
with SceneIsLoading(scene):
if file_path == "environment:creative":
try:
with SceneIsLoading(scene):
if file_path == "environment:creative":
return await load_scene_from_data(
scene, creative_environment(), conv_client, reset=True
)
ext = os.path.splitext(file_path)[1].lower()
if ext in [".jpg", ".png", ".jpeg", ".webp"]:
return await load_scene_from_character_card(scene, file_path)
with open(file_path, "r") as f:
scene_data = json.load(f)
return await load_scene_from_data(
scene, creative_environment(), conv_client, reset=True
scene, scene_data, conv_client, reset, name=file_path
)
ext = os.path.splitext(file_path)[1].lower()
if ext in [".jpg", ".png", ".jpeg", ".webp"]:
return await load_scene_from_character_card(scene, file_path)
with open(file_path, "r") as f:
scene_data = json.load(f)
return await load_scene_from_data(
scene, scene_data, conv_client, reset, name=file_path
)
finally:
await scene.add_to_recent_scenes()
async def load_scene_from_character_card(scene, file_path):
@@ -229,11 +207,18 @@ async def load_scene_from_data(
events.ArchiveEvent(scene=scene, event_type="archive_add", text=ah["text"], ts=ts)
)
for character_name, character_data in scene_data.get("inactive_characters", {}).items():
scene.inactive_characters[character_name] = Character(**character_data)
for character_name, cs in scene.character_states.items():
scene.set_character_state(character_name, cs)
for character_data in scene_data["characters"]:
character = Character(**character_data)
if character.name in scene.inactive_characters:
scene.inactive_characters.pop(character.name)
if not character.is_player:
agent = instance.get_agent("conversation", client=conv_client)
actor = Actor(character, agent)

View File

@@ -23,6 +23,7 @@ from talemate.emit import emit
from talemate.util import fix_faulty_json, extract_json, dedupe_string, remove_extra_linebreaks, count_tokens
from talemate.config import load_config
import talemate.thematic_generators as thematic_generators
from talemate.context import rerun_context
import talemate.instance as instance
@@ -313,6 +314,7 @@ class Prompt:
ctx = {
"bot_token": "<|BOT|>",
"thematic_generator": thematic_generators.ThematicGenerator(),
"rerun_context": rerun_context.get(),
}
env.globals["render_template"] = self.render_template
@@ -578,6 +580,11 @@ class Prompt:
# strip comments
try:
# if response starts with ```json and ends with ```
# then remove those
if response.startswith("```json") and response.endswith("```"):
response = response[7:-3]
try:
response = json.loads(response)
return response

View File

@@ -91,4 +91,12 @@ Always contain dialogue in quotation marks. For example, {{ talking_character.na
<|CLOSE_SECTION|>
{% if scene.count_character_messages(talking_character) < 5 %}Use an informal and colloquial register with a conversational tone. Overall, {{ talking_character.name }}'s dialog is Informal, conversational, natural, and spontaneous, with a sense of immediacy. Flesh out additional details by describing {{ talking_character.name }}'s actions and mannerisms within asterisks, e.g. *{{ talking_character.name }} smiles*.
{% endif -%}
{% if rerun_context and rerun_context.direction -%}
{% if rerun_context.method == 'replace' -%}
Final instructions for generating the next line of dialogue: {{ rerun_context.direction }}
{% elif rerun_context.method == 'edit' and rerun_context.message -%}
Edit and respond with your changed version of the following line of dialogue: {{ rerun_context.message }}
Requested changes: {{ rerun_context.direction }}
{% endif -%}
{% endif -%}
{{ bot_token}}{{ talking_character.name }}:{{ partial_message }}

View File

@@ -25,6 +25,7 @@ Use an informal and colloquial register with a conversational tone. Overall, the
Narration style should be that of a 90s point and click adventure game. You are omniscient and can describe the scene in detail.
Only generate new narration. {{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
[$REPETITION|Narration is getting repetitive. Try to choose different words to break up the repetitive text.]
<|CLOSE_SECTION|>

View File

@@ -0,0 +1,20 @@
{% block rendered_context -%}
{% include "extra-context.jinja2" %}
<|SECTION:CONTEXT|>
{{ character.sheet }}
{{ character.description }}
<|CLOSE_SECTION|>
{% endblock -%}
<|SECTION:SCENE|>
{% for scene_context in scene.context_history(budget=max_tokens-300-count_tokens(self.rendered_context())) -%}
{{ scene_context }}
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Narrate the entrance of {{ character.name }} into the scene: {% if direction %} {{ direction }}{% else %}Make a creative decision on how {{ character.name }} enters the scene. It must be in line with the content so far.{% endif %}
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>

View File

@@ -0,0 +1,20 @@
{% block rendered_context -%}
{% include "extra-context.jinja2" %}
<|SECTION:CONTEXT|>
{{ character.sheet }}
{{ character.description }}
<|CLOSE_SECTION|>
{% endblock -%}
<|SECTION:SCENE|>
{% for scene_context in scene.context_history(budget=max_tokens-300-count_tokens(self.rendered_context())) -%}
{{ scene_context }}
{% endfor %}
<|CLOSE_SECTION|>
<|SECTION:TASK|>
Narrate the exit of {{ character.name }} from the scene:{% if direction %} {{ direction }}{% else %}Make a creative decision on how {{ character.name }} leaves the scene. It must be in line with the content so far.{% endif %}
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>

View File

@@ -31,5 +31,6 @@ Use an informal and colloquial register with a conversational tone. Overall, the
Write 2 to 3 sentences.
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>
{{ bot_token }}At the end of the dialogue,

View File

@@ -35,5 +35,6 @@ Directions for new narration: {{ narrative_direction }}
{% endif %}
Write 2 to 4 sentences. {{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>
{{ set_prepared_response("*") }}

View File

@@ -31,5 +31,6 @@ Question: {{ query }}
Content Context: This is a specific scene from {{ scene.context }}
Your answer should be in the style of short, concise narration that fits the context of the scene. (1 to 2 sentences)
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>
{% if query.endswith("?") -%}Answer: {% endif -%}

View File

@@ -11,5 +11,6 @@
<|SECTION:TASK|>
Provide a visual description of what is currently happening in the scene. Don't progress the scene.
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
<|CLOSE_SECTION|>
{{ bot_token }}At the end of the scene we currently see that

View File

@@ -19,7 +19,7 @@ Directions for new narration: {{ narrative }}
{% endif %}
{{ extra_instructions }}
{% include "rerun-context.jinja2" -%}
Write 1 to 3 sentences.
<|CLOSE_SECTION|>
{{ bot_token }}{{ time_passed }}:

View File

@@ -0,0 +1,8 @@
{% if rerun_context and rerun_context.direction -%}
{% if rerun_context.method == 'replace' -%}
Final instructions: {{ rerun_context.direction }}
{% elif rerun_context.method == 'edit' and rerun_context.message -%}
Edit and respond with your changed version of the following narration: {{ rerun_context.message }}
Requested changes: {{ rerun_context.direction }}
{% endif -%}
{% endif -%}

View File

@@ -1,17 +1,28 @@
<|SECTION:DIALOGUE|>
{{ dialogue }}
{% if extra_context -%}
<|SECTION:PREVIOUS CONTEXT|>
{{ extra_context }}
<|CLOSE_SECTION|>
{% endif -%}
<|SECTION:TASK|>
Question: What happens within the dialogue? Summarize into narrative description.
Question: What happens explicitly within the dialogue section alpha below? Summarize into narrative description.
Content Context: This is a specific scene from {{ scene.context }}
Use an informal and colloquial register with a conversational tone. Overall, the narrative is informal, conversational, natural, and spontaneous, with a sense of immediacy.
{% if summarization_method == "long" -%}
This should be a detailed summary of the dialogue, including all the juicy details.
{% elif summarization_method == "short" -%}
This should be a short and concise summary of the dialogue, including only the most important details. 1 - 3 sentences.
This should be a short and specific summary of the dialogue, including the most important details. 2 - 3 sentences.
{% endif -%}
Expected Answer: A summarized narrative description of the dialogue that can be inserted into the ongoing story in place of the dialogue.
<|CLOSE_SECTION|>
YOU MUST ONLY SUMMARIZE THE CONTENT IN DIALOGUE SECTION ALPHA.
Expected Answer: A summarized narrative description of the dialogue section alpha, that can be inserted into the ongoing story in place of the dialogue.
{% if extra_instructions -%}
{{ extra_instructions }}
{% endif -%}
<|CLOSE_SECTION|>
<|SECTION:DIALOGUE SECTION ALPHA|>
{{ dialogue }}
<|CLOSE_SECTION|>
<|SECTION:SUMMARIZATION OF DIALOGUE SECTION ALPHA|>
{{ bot_token }}

View File

@@ -22,6 +22,17 @@ class Asset(pydantic.BaseModel):
file_type: str
media_type: str
def to_base64(self, asset_directory:str) -> str:
"""
Returns the asset as a base64 encoded string.
"""
asset_path = os.path.join(asset_directory, f"{self.id}.{self.file_type}")
with open(asset_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
class SceneAssets:
def __init__(self, scene:Scene):
@@ -52,7 +63,11 @@ class SceneAssets:
"""
Returns the path to the asset with the given id.
"""
return os.path.join(self.asset_directory, f"{asset_id}.{self.assets[asset_id].file_type}")
try:
return os.path.join(self.asset_directory, f"{asset_id}.{self.assets[asset_id].file_type}")
except KeyError:
log.error("asset_path", asset_id=asset_id, assets=self.assets)
return None
def dict(self, *args, **kwargs):
return {

View File

@@ -131,6 +131,9 @@ async def websocket_endpoint(websocket, path):
elif action_type == "request_scene_history":
log.info("request_scene_history")
handler.request_scene_history()
elif action_type == "request_assets":
log.info("request_assets", data=data)
handler.request_assets(data.get("assets"))
elif action_type == "edit_message":
log.info("edit_message", data=data)
handler.edit_message(data.get("id"), data.get("text"))

View File

@@ -1,8 +1,10 @@
import pydantic
import structlog
from talemate import VERSION
from talemate.client.registry import CLIENT_CLASSES
from talemate.config import Config as AppConfigData, load_config, save_config
from talemate.client.model_prompts import model_prompt
from talemate.emit import emit
log = structlog.get_logger("talemate.server.config")
@@ -15,6 +17,14 @@ class DefaultCharacterPayload(pydantic.BaseModel):
description: str
color: str = "#3362bb"
class SetLLMTemplatePayload(pydantic.BaseModel):
template_file: str
model: str
class DetermineLLMTemplatePayload(pydantic.BaseModel):
model: str
class ConfigPlugin:
router = "config"
@@ -77,4 +87,79 @@ class ConfigPlugin:
"type": "config",
"action": "save_default_character_complete",
})
async def handle_request_std_llm_templates(self, data):
log.info("Requesting std llm templates")
self.websocket_handler.queue_put({
"type": "config",
"action": "std_llm_templates",
"data": {
"templates": model_prompt.std_templates,
}
})
async def handle_set_llm_template(self, data):
payload = SetLLMTemplatePayload(**data["data"])
copied_to = model_prompt.create_user_override(payload.template_file, payload.model)
log.info("Copied template", copied_to=copied_to, template=payload.template_file, model=payload.model)
prompt_template_example, prompt_template_file = model_prompt(payload.model, "sysmsg", "prompt<|BOT|>{LLM coercion}")
log.info("Prompt template example", prompt_template_example=prompt_template_example, prompt_template_file=prompt_template_file)
self.websocket_handler.queue_put({
"type": "config",
"action": "set_llm_template_complete",
"data": {
"prompt_template_example": prompt_template_example,
"has_prompt_template": True if prompt_template_example else False,
"template_file": prompt_template_file,
}
})
async def handle_determine_llm_template(self, data):
payload = DetermineLLMTemplatePayload(**data["data"])
log.info("Determining LLM template", model=payload.model)
template = model_prompt.query_hf_for_prompt_template_suggestion(payload.model)
log.info("Template suggestion", template=template)
if not template:
emit("status", message="No template found for model", status="warning")
else:
await self.handle_set_llm_template({
"data": {
"template_file": template,
"model": payload.model,
}
})
self.websocket_handler.queue_put({
"type": "config",
"action": "determine_llm_template_complete",
"data": {
"template": template,
}
})
async def handle_request_client_types(self, data):
log.info("Requesting client types")
clients = {
client_type: CLIENT_CLASSES[client_type].Meta().model_dump() for client_type in CLIENT_CLASSES
}
self.websocket_handler.queue_put({
"type": "config",
"action": "client_types",
"data": clients,
})

View File

@@ -10,7 +10,9 @@ from talemate.config import load_config, save_config, SceneAssetUpload
from talemate.emit import Emission, Receiver, abort_wait_for_input, emit
from talemate.files import list_scenes_directory
from talemate.load import load_scene, load_scene_from_data, load_scene_from_character_card
from talemate.scene_assets import Asset
from talemate.client.registry import CLIENT_CLASSES
from talemate.server import character_creator
from talemate.server import character_importer
@@ -79,6 +81,7 @@ class WebsocketHandler(Receiver):
**client_config
)
except TypeError as e:
raise
log.error("Error connecting to client", client_name=client_name, e=e)
continue
@@ -100,6 +103,7 @@ class WebsocketHandler(Receiver):
if not client:
# select first client
print("selecting first client", self.llm_clients)
client = list(self.llm_clients.values())[0]["client"]
agent_config["client"] = client.name
@@ -174,32 +178,18 @@ class WebsocketHandler(Receiver):
for client in clients:
client.pop("status", None)
client_cls = CLIENT_CLASSES.get(client["type"])
if client["type"] in ["textgenwebui", "lmstudio"]:
try:
max_token_length = int(client.get("max_token_length", 2048))
except ValueError:
continue
client.pop("model", None)
self.llm_clients[client["name"]] = {
"type": client["type"],
"api_url": client["apiUrl"],
"name": client["name"],
"max_token_length": max_token_length,
}
elif client["type"] == "openai":
client.pop("model_name", None)
client.pop("apiUrl", None)
self.llm_clients[client["name"]] = {
"type": "openai",
"name": client["name"],
"model": client.get("model", client.get("model_name")),
"max_token_length": client.get("max_token_length"),
}
if not client_cls:
log.error("Client type not found", client=client)
continue
client_config = self.llm_clients[client["name"]] = {
"name": client["name"],
"type": client["type"],
}
for dfl_key in client_cls.Meta().defaults.dict().keys():
client_config[dfl_key] = client.get(dfl_key)
# find clients that have been removed
removed = existing - set(self.llm_clients.keys())
@@ -223,6 +213,8 @@ class WebsocketHandler(Receiver):
self.connect_llm_clients()
save_config(self.config)
instance.sync_emit_clients_status()
def configure_agents(self, agents):
self.agents = {typ: {} for typ in instance.agent_types()}
@@ -427,7 +419,9 @@ class WebsocketHandler(Receiver):
"status": emission.status,
"data": emission.data,
"max_token_length": client.max_token_length if client else 4096,
"apiUrl": getattr(client, "api_url", None) if client else None,
"api_url": getattr(client, "api_url", None) if client else None,
"api_url": getattr(client, "api_url", None) if client else None,
"api_key": getattr(client, "api_key", None) if client else None,
}
)
@@ -563,7 +557,7 @@ class WebsocketHandler(Receiver):
)
async def request_client_status(self):
instance.emit_clients_status()
await instance.emit_clients_status()
def request_scene_assets(self, asset_ids:list[str]):
scene_assets = self.scene.assets
@@ -580,6 +574,44 @@ class WebsocketHandler(Receiver):
"media_type": scene_assets.get_asset(asset_id).media_type,
}
)
def request_assets(self, assets:list[dict]):
# way to request scene assets without loading the scene
#
# assets is a list of dicts with keys:
# path must be turned into absolute path
# path must begin with Scene.scenes_dir()
_assets = {}
for asset_dict in assets:
try:
asset_id, asset = self._asset(**asset_dict)
except Exception as exc:
log.error("request_assets", error=traceback.format_exc(), **asset_dict)
continue
_assets[asset_id] = asset
self.queue_put(
{
"type": "assets",
"assets": _assets,
}
)
def _asset(self, path: str, **asset):
absolute_path = os.path.abspath(path)
if not absolute_path.startswith(Scene.scenes_dir()):
log.error("_asset", error="Invalid path", path=absolute_path, scenes_dir=Scene.scenes_dir())
return
asset_path = os.path.join(os.path.dirname(absolute_path), "assets")
asset = Asset(**asset)
return asset.id, {
"base64": asset.to_base64(asset_path),
"media_type": asset.media_type,
}
def add_scene_asset(self, data:dict):
asset_upload = SceneAssetUpload(**data)

View File

@@ -29,6 +29,7 @@ class SetCharacterDetailReinforcementPayload(pydantic.BaseModel):
class CharacterDetailReinforcementPayload(pydantic.BaseModel):
name: str
question: str
reset: bool = False
class SaveWorldEntryPayload(pydantic.BaseModel):
id:str
@@ -48,6 +49,7 @@ class SetWorldEntryReinforcementPayload(pydantic.BaseModel):
class WorldEntryReinforcementPayload(pydantic.BaseModel):
question: str
reset: bool = False
class QueryContextDBPayload(pydantic.BaseModel):
query: str
@@ -230,7 +232,13 @@ class WorldStateManagerPlugin:
payload = CharacterDetailReinforcementPayload(**data)
await self.world_state_manager.run_detail_reinforcement(payload.name, payload.question)
log.debug("Run character detail reinforcement", name=payload.name, question=payload.question, reset=payload.reset)
await self.world_state_manager.run_detail_reinforcement(
payload.name,
payload.question,
reset=payload.reset
)
self.websocket_handler.queue_put({
"type": "world_state_manager",
@@ -328,7 +336,7 @@ class WorldStateManagerPlugin:
async def handle_run_world_state_reinforcement(self, data):
payload = WorldEntryReinforcementPayload(**data)
await self.world_state_manager.run_detail_reinforcement(None, payload.question)
await self.world_state_manager.run_detail_reinforcement(None, payload.question, payload.reset)
self.websocket_handler.queue_put({
"type": "world_state_manager",

37
src/talemate/status.py Normal file
View File

@@ -0,0 +1,37 @@
from talemate.emit import emit
import structlog
__all__ = [
"set_loading",
"LoadingStatus",
]
log = structlog.get_logger("talemate.status")
class set_loading:
def __init__(self, message, set_busy:bool=True):
self.message = message
self.set_busy = set_busy
def __call__(self, fn):
async def wrapper(*args, **kwargs):
if self.set_busy:
emit("status", message=self.message, status="busy")
try:
return await fn(*args, **kwargs)
finally:
emit("status", message="", status="idle")
return wrapper
class LoadingStatus:
def __init__(self, max_steps:int):
self.max_steps = max_steps
self.current_step = 0
def __call__(self, message:str):
self.current_step += 1
emit("status", message=f"{message} [{self.current_step}/{self.max_steps}]", status="busy")

View File

@@ -29,9 +29,10 @@ from talemate.exceptions import ExitScene, RestartSceneLoop, ResetScene, Talemat
from talemate.world_state import WorldState
from talemate.world_state.manager import WorldStateManager
from talemate.game_state import GameState
from talemate.config import SceneConfig, load_config
from talemate.config import SceneConfig, load_config, Config
from talemate.scene_assets import SceneAssets
from talemate.client.context import ClientContext, ConversationContext
from talemate.context import rerun_context
import talemate.automated_action as automated_action
@@ -84,28 +85,6 @@ class Character:
self.details = details or {}
self.cover_image = kwargs.get("cover_image")
@property
def pronoun(self):
if self.gender.lower() == "female":
return "her"
elif self.gender.lower() == "male":
return "his"
elif self.gender.lower() == "neutral":
return "their"
else:
return "its"
@property
def pronoun_2(self):
if self.gender.lower() == "female":
return "she"
if self.gender.lower() == "male":
return "he"
if self.gender.lower() == "neutral":
return "they"
else:
return "it"
@property
def persona(self):
return self.description
@@ -379,6 +358,7 @@ class Character:
for key, detail in self.details.items():
items.append({
"text": f"{self.name} - {key}: {detail}",
"id": f"{self.name}.{key}",
"meta": {
"character": self.name,
"typ": "details",
@@ -448,6 +428,7 @@ class Character:
items.append({
"text": f"{self.name} - {detail}: {value}",
"id": f"{self.name}.{detail}",
"meta": {
"character": self.name,
"typ": "details",
@@ -576,7 +557,7 @@ class Actor:
def history(self):
return self.scene.history
async def talk(self, editor: Optional[Helper] = None):
async def talk(self):
"""
Set the message to be sent to the AI
"""
@@ -592,7 +573,7 @@ class Actor:
)
with ClientContext(conversation=conversation_context):
messages = await self.agent.converse(self, editor=editor)
messages = await self.agent.converse(self)
return messages
@@ -603,7 +584,7 @@ class Player(Actor):
ai_controlled = 0
async def talk(
self, message: Union[str, None] = None, editor: Optional[Helper] = None
self, message: Union[str, None] = None
):
"""
Set the message to be sent to the AI
@@ -619,7 +600,7 @@ class Player(Actor):
if not self.agent:
self.agent = self.scene.get_helper("conversation").agent
return await super().talk(editor=editor)
return await super().talk()
if not message:
# Display scene history length before the player character name
@@ -655,6 +636,16 @@ class Scene(Emitter):
ExitScene = ExitScene
@classmethod
def scenes_dir(cls):
relative_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"..",
"..",
"scenes",
)
return os.path.abspath(relative_path)
def __init__(self):
self.actors = []
self.helpers = []
@@ -662,6 +653,7 @@ class Scene(Emitter):
self.archived_history = []
self.goals = []
self.character_states = {}
self.inactive_characters = {}
self.assets = SceneAssets(scene=self)
self.description = ""
self.intro = ""
@@ -697,8 +689,9 @@ class Scene(Emitter):
self.Actor = Actor
self.Character = Character
# TODO: deprecate
self.automated_actions = {}
self.active_pins = []
# Add an attribute to store the most recent AI Actor
self.most_recent_ai_actor = None
@@ -761,13 +754,11 @@ class Scene(Emitter):
if isinstance(self.history[idx], CharacterMessage):
return self.history[idx].character_name
@property
def save_dir(self):
saves_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"..",
"..",
"scenes",
self.scenes_dir(),
self.project_name,
)
@@ -776,6 +767,13 @@ class Scene(Emitter):
return saves_dir
@property
def full_path(self):
if not self.filename:
return None
return os.path.join(self.save_dir, self.filename)
@property
def template_dir(self):
return os.path.join(self.save_dir, "templates")
@@ -1079,6 +1077,9 @@ class Scene(Emitter):
"""
Returns the character with the given name if it exists
"""
if character_name in self.inactive_characters:
return self.inactive_characters[character_name]
for actor in self.actors:
if not partial and actor.character.name.lower() == character_name.lower():
@@ -1228,9 +1229,9 @@ class Scene(Emitter):
# collect context, ignore where end > len(history) - count
for i in range(len(self.archived_history) - 1, -1, -1):
end = self.archived_history[i].get("end")
start = self.archived_history[i].get("start")
archive_history_entry = self.archived_history[i]
end = archive_history_entry.get("end")
start = archive_history_entry.get("start")
if end is None:
continue
@@ -1238,10 +1239,17 @@ class Scene(Emitter):
if start > len(self.history) - count:
continue
if count_tokens(parts_context) + count_tokens(self.archived_history[i]["text"]) > budget_context:
try:
time_message = util.iso8601_diff_to_human(archive_history_entry["ts"], self.ts)
text = f"{time_message}: {archive_history_entry['text']}"
except Exception as e:
log.error("context_history", error=e, traceback=traceback.format_exc())
text = archive_history_entry["text"]
if count_tokens(parts_context) + count_tokens(text) > budget_context:
break
parts_context.insert(0, self.archived_history[i]["text"])
parts_context.insert(0, text)
if count_tokens(parts_context + parts_dialogue) < 1024:
intro = self.get_intro()
@@ -1250,7 +1258,7 @@ class Scene(Emitter):
return list(map(str, parts_context)) + list(map(str, parts_dialogue))
async def rerun(self, editor: Optional[Helper] = None):
async def rerun(self):
"""
Rerun the most recent AI response, remove their previous message from the history,
and call talk() for the most recent AI Character.
@@ -1280,9 +1288,14 @@ class Scene(Emitter):
if message.source == "player":
return
current_rerun_context = rerun_context.get()
if current_rerun_context:
current_rerun_context.message = message.message
if isinstance(message, CharacterMessage):
self.history.pop()
await self._rerun_character_message(message, editor=editor)
await self._rerun_character_message(message)
elif isinstance(message, NarratorMessage):
self.history.pop()
await self._rerun_narrator_message(message)
@@ -1317,6 +1330,9 @@ class Scene(Emitter):
elif source == "narrate_dialogue":
character = self.get_character(arg)
new_message = await narrator.agent.narrate_after_dialogue(character)
elif source == "narrate_character_entry":
character = self.get_character(arg)
new_message = await narrator.agent.narrate_character_entry(character)
elif source == "__director__":
director = self.get_helper("director").agent
await director.direct_scene(None, None)
@@ -1368,25 +1384,25 @@ class Scene(Emitter):
self.push_history(new_message)
emit("director", new_message, character=character)
async def _rerun_character_message(self, message, editor=None):
async def _rerun_character_message(self, message):
character_name = message.split(":")[0]
character = self.get_character(character_name)
if character.is_player:
emit("system", "Cannot rerun player's message")
return
emit("remove_message", "", id=message.id)
# Call talk() for the most recent AI Actor with the same editor parameter
new_messages = await self.most_recent_ai_actor.talk(editor=editor)
# Call talk() for the most recent AI Actor
actor = character.actor
new_messages = await actor.talk()
# Print the new messages
for item in new_messages:
character = self.most_recent_ai_actor.agent.character
emit("character", item, character=character)
await asyncio.sleep(0)
@@ -1418,6 +1434,14 @@ class Scene(Emitter):
break
def can_auto_save(self):
"""
A scene can be autosaved if it has a filename set and is not immutable_save
"""
return self.filename and not self.immutable_save
def emit_status(self):
player_character = self.get_player_character()
emit(
@@ -1428,6 +1452,7 @@ class Scene(Emitter):
"environment": self.environment,
"scene_config": self.scene_config,
"player_character_name": player_character.name if player_character else None,
"inactive_characters": list(self.inactive_characters.keys()),
"context": self.context,
"assets": self.assets.dict(),
"characters": [actor.character.serialize for actor in self.actors],
@@ -1435,6 +1460,7 @@ class Scene(Emitter):
"saved": self.saved,
"auto_save": self.auto_save,
"auto_progress": self.auto_progress,
"can_auto_save": self.can_auto_save(),
"game_state": self.game_state.model_dump(),
"active_pins": [pin.model_dump() for pin in self.active_pins],
},
@@ -1694,7 +1720,8 @@ class Scene(Emitter):
async def _run_creative_loop(self, init: bool = True):
self.system_message("Creative mode")
emit("status", message="Switched to scene editor", status="info")
if init:
emit("clear_screen", "")
self.narrator_message(self.description)
@@ -1783,6 +1810,7 @@ class Scene(Emitter):
"archived_history": scene.archived_history,
"character_states": scene.character_states,
"characters": [actor.character.serialize for actor in scene.actors],
"inactive_characters": {name: character.serialize for name, character in scene.inactive_characters.items()},
"goal": scene.goal,
"goals": scene.goals,
"context": scene.context,
@@ -1805,7 +1833,16 @@ class Scene(Emitter):
self.saved = True
self.emit_status()
# add this scene to recent scenes in config
await self.add_to_recent_scenes()
async def add_to_recent_scenes(self):
log.debug("add_to_recent_scenes", filename=self.filename)
config = Config(**self.config)
config.recent_scenes.push(self)
config.save()
async def commit_to_memory(self):
# will recommit scene to long term memory

View File

@@ -93,6 +93,9 @@ class WorldStateManager:
for character in self.scene.get_characters():
characters.characters[character.name] = CharacterSelect(name=character.name, active=True, is_player=character.is_player)
for character in self.scene.inactive_characters.values():
characters.characters[character.name] = CharacterSelect(name=character.name, active=False, is_player=character.is_player)
return characters
async def get_character_details(self, character_name:str) -> CharacterDetails:
@@ -268,7 +271,7 @@ class WorldStateManager:
return reinforcement
async def run_detail_reinforcement(self, character_name:str, question:str):
async def run_detail_reinforcement(self, character_name:str, question:str, reset:bool=False):
"""
Executes the detail reinforcement for a specific character and question.
@@ -277,7 +280,7 @@ class WorldStateManager:
question: The query/question that the reinforcement corresponds to.
"""
world_state_agent = get_agent("world_state")
await world_state_agent.update_reinforcement(question, character_name)
await world_state_agent.update_reinforcement(question, character_name, reset=reset)
async def delete_detail_reinforcement(self, character_name:str, question:str):
"""

View File

@@ -12,7 +12,7 @@
"core-js": "^3.8.3",
"roboto-fontface": "*",
"vue": "^3.2.13",
"vuetify": "^3.3.11",
"vuetify": "^3.5.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {
@@ -10442,9 +10442,9 @@
"dev": true
},
"node_modules/vuetify": {
"version": "3.3.14",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.14.tgz",
"integrity": "sha512-5kGnahj/cX5989bV9XM432k/BJ11fRdJ3CCdISjo1auCz+rLEeLJdjMeqyCJVd0FZsWcE1Z8799s9sLdLM3Deg==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.5.0.tgz",
"integrity": "sha512-zpZFZoJE9c8QlHc8s9zowKzMUTjytdzz2PQpZPezVENm0Jp+KBi+KooZGxvj7l+YfeFdKOcSjht7nEptSSMPMg==",
"engines": {
"node": "^12.20 || >=14.13"
},
@@ -10454,10 +10454,10 @@
},
"peerDependencies": {
"typescript": ">=4.7",
"vite-plugin-vuetify": "^1.0.0-alpha.12",
"vue": "^3.2.0",
"vite-plugin-vuetify": ">=1.0.0-alpha.12",
"vue": "^3.3.0",
"vue-i18n": "^9.0.0",
"webpack-plugin-vuetify": "^2.0.0-alpha.11"
"webpack-plugin-vuetify": ">=2.0.0-alpha.11"
},
"peerDependenciesMeta": {
"typescript": {
@@ -19323,9 +19323,9 @@
"dev": true
},
"vuetify": {
"version": "3.3.14",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.14.tgz",
"integrity": "sha512-5kGnahj/cX5989bV9XM432k/BJ11fRdJ3CCdISjo1auCz+rLEeLJdjMeqyCJVd0FZsWcE1Z8799s9sLdLM3Deg==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.5.0.tgz",
"integrity": "sha512-zpZFZoJE9c8QlHc8s9zowKzMUTjytdzz2PQpZPezVENm0Jp+KBi+KooZGxvj7l+YfeFdKOcSjht7nEptSSMPMg==",
"requires": {}
},
"watchpack": {

View File

@@ -12,7 +12,7 @@
"core-js": "^3.8.3",
"roboto-fontface": "*",
"vue": "^3.2.13",
"vuetify": "^3.3.11",
"vuetify": "^3.5.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {

View File

@@ -14,6 +14,11 @@
<v-icon v-else color="green" size="14">mdi-checkbox-blank-circle</v-icon>
{{ client.name }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption" v-if="client.data.error_action != null">
<v-btn class="mt-1 mb-1" variant="tonal" :prepend-icon="client.data.error_action.icon" size="x-small" color="warning" @click.stop="callErrorAction(client, client.data.error_action)">
{{ client.data.error_action.title }}
</v-btn>
</v-list-item-subtitle>
<v-list-item-subtitle class="text-caption">
{{ client.model_name }}
</v-list-item-subtitle>
@@ -27,7 +32,7 @@
v-model="client.max_token_length"
:min="1024"
:max="128000"
:step="512"
:step="1024"
@update:modelValue="saveClientDelayed(client)"
@click.stop
density="compact"
@@ -35,7 +40,7 @@
</div>
<v-list-item-subtitle class="text-center">
<v-tooltip text="No LLM prompt template for this model. Using default. Templates can be added in ./templates/llm-prompt" v-if="client.status === 'idle' && client.data && !client.data.has_prompt_template" max-width="200">
<v-tooltip text="No LLM prompt template for this model. Using default. Templates can be added in ./templates/llm-prompt" v-if="client.status === 'idle' && client.data && !client.data.has_prompt_template && client.data.meta.requires_prompt_template" max-width="200">
<template v-slot:activator="{ props }">
<v-icon x-size="14" class="mr-1" v-bind="props" color="orange">mdi-alert</v-icon>
</template>
@@ -64,7 +69,7 @@
</v-list>
<ClientModal :dialog="state.dialog" :formTitle="state.formTitle" @save="saveClient" @error="propagateError" @update:dialog="updateDialog"></ClientModal>
<v-alert type="warning" variant="tonal" v-if="state.clients.length === 0">You have no LLM clients configured. Add one.</v-alert>
<v-btn @click="openModal" prepend-icon="mdi-plus-box">Add client</v-btn>
<v-btn @click="openModal" elevation="0" prepend-icon="mdi-plus-box">Add client</v-btn>
</div>
</template>
@@ -85,7 +90,7 @@ export default {
currentClient: {
name: '',
type: '',
apiUrl: '',
api_url: '',
model_name: '',
max_token_length: 4096,
data: {
@@ -107,7 +112,19 @@ export default {
state: this.state
};
},
emits: [
'clients-updated',
'client-assigned',
'open-app-config',
],
methods: {
callErrorAction(client, action) {
if(action.action_name === 'openAppConfig') {
this.$emit('open-app-config', ...action.arguments);
}
},
configurationRequired() {
if(this.state.clients.length === 0) {
return true;
@@ -129,7 +146,7 @@ export default {
this.state.currentClient = {
name: 'TextGenWebUI',
type: 'textgenwebui',
apiUrl: 'http://localhost:5000',
api_url: 'http://localhost:5000',
model_name: '',
max_token_length: 4096,
data: {
@@ -201,10 +218,12 @@ export default {
if (client && !client.dirty) {
// Update the model name of the client
client.model_name = data.model_name;
client.model = client.model_name;
client.type = data.message;
client.status = data.status;
client.max_token_length = data.max_token_length;
client.apiUrl = data.apiUrl;
client.api_url = data.api_url;
client.api_key = data.api_key;
client.data = data.data;
} else if(!client) {
console.log("Adding new client", data);
@@ -212,10 +231,12 @@ export default {
this.state.clients.push({
name: data.name,
model_name: data.model_name,
model: data.model_name,
type: data.message,
status: data.status,
max_token_length: data.max_token_length,
apiUrl: data.apiUrl,
api_url: data.api_url,
api_key: data.api_key,
data: data.data,
});
// sort the clients by name

View File

@@ -35,11 +35,15 @@
</div>
<div v-for="(action_config, config_key) in agent.data.actions[key].config" :key="config_key">
<div v-if="action.enabled">
<!-- render config widgets based on action_config.type (int, str, bool, float) -->
<v-text-field v-if="action_config.type === 'text' && action_config.choices === null" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact" @update:modelValue="save(true)"></v-text-field>
<v-autocomplete v-else-if="action_config.type === 'text' && action_config.choices !== null" v-model="action.config[config_key].value" :items="action_config.choices" :label="action_config.label" :hint="action_config.description" density="compact" item-title="label" item-value="value" @update:modelValue="save(false)"></v-autocomplete>
<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 @update:modelValue="save(true)"></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" @update:modelValue="save(false)"></v-checkbox>
<!-- render config widgets based on action_config.type (int, str, bool, float) -->
<v-text-field v-if="action_config.type === 'text' && action_config.choices === null" v-model="action.config[config_key].value" :label="action_config.label" :hint="action_config.description" density="compact" @update:modelValue="save(true)"></v-text-field>
<v-autocomplete v-else-if="action_config.type === 'text' && action_config.choices !== null" v-model="action.config[config_key].value" :items="action_config.choices" :label="action_config.label" :hint="action_config.description" density="compact" item-title="label" item-value="value" @update:modelValue="save(false)"></v-autocomplete>
<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 @update:modelValue="save(true)"></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" @update:modelValue="save(false)"></v-checkbox>
<v-alert v-if="action_config.note != null" variant="outlined" density="compact" color="grey-darken-1" icon="mdi-information">
{{ action_config.note }}
</v-alert>
</div>
</div>
</v-card-text>

View File

@@ -264,10 +264,15 @@ export default {
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets', 'requestAppConfig'],
methods: {
show() {
show(tab, page) {
this.requestAppConfig();
this.dialog = true;
if(tab) {
this.tab = tab;
if(page) {
this[tab + 'PageSelected'] = page;
}
}
},
exit() {
this.dialog = false

View File

@@ -19,7 +19,7 @@
</v-row>
<v-combobox :items="content_context
" label="Content Context" v-model="scenario_context"></v-combobox>
<v-textarea label="Character prompt" v-model="character_prompt"></v-textarea>
<v-textarea label="Character prompt" v-model="character_prompt" :placeholder="character_prompt_placeholder"></v-textarea>
</v-card-text>
</v-card>
</template>
@@ -175,11 +175,7 @@
</template>
<script>
import { VStepper } from 'vuetify/labs/VStepper'
export default {
components: {
VStepper,
},
name: 'CharacterCreator',
data() {
return {
@@ -194,9 +190,9 @@ export default {
'Add to World',
],
content_context: [
"a fun and engaging slice of life story aimed at an adult audience.",
"a fun and engaging slice of life story",
],
scenario_context: "a fun and engaging slice of life story aimed at an adult audience.",
scenario_context: "a fun and engaging slice of life story",
templates: ["human"],
selected_template: "human",
base_attributes: {},
@@ -210,7 +206,8 @@ export default {
notification_text: '',
is_player_character: false,
use_spice: 0.1,
character_prompt: 'A 19-year-old boy who just did something embarrassing in front of his crush.',
character_prompt: '',
character_prompt_placeholder: 'A short description of the character you want to generate.',
character: null,
description: "",
generating: false,

View File

@@ -1,5 +1,10 @@
<template>
<v-alert variant="text" closable type="info" icon="mdi-chat-outline" elevation="0" density="compact" @click:close="deleteMessage()" @mouseover="hovered=true" @mouseleave="hovered=false">
<v-alert variant="text" type="info" icon="mdi-chat-outline" elevation="0" density="compact" @mouseover="hovered=true" @mouseleave="hovered=false">
<template v-slot:close>
<v-btn size="x-small" icon @click="deleteMessage">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
<v-alert-title :style="{ color: color }" class="text-subtitle-1">
{{ character }}
</v-alert-title>

View File

@@ -1,5 +1,5 @@
<template>
<v-dialog v-model="localDialog" persistent max-width="600px">
<v-dialog v-model="localDialog" max-width="800px">
<v-card>
<v-card-title>
<v-icon>mdi-network-outline</v-icon>
@@ -9,31 +9,46 @@
<v-container>
<v-row>
<v-col cols="6">
<v-select v-model="client.type" :disabled="!typeEditable()" :items="['openai', 'textgenwebui', 'lmstudio']" label="Client Type" @update:model-value="resetToDefaults"></v-select>
<v-select v-model="client.type" :disabled="!typeEditable()" :items="clientChoices" label="Client Type" @update:model-value="resetToDefaults"></v-select>
</v-col>
<v-col cols="6">
<v-text-field v-model="client.name" label="Client Name"></v-text-field>
</v-col>
</v-row>
<v-row v-if="clientMeta().experimental">
<v-col cols="12">
<v-alert type="warning" variant="text" density="compact" icon="mdi-flask" outlined>{{ clientMeta().experimental }}</v-alert>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-text-field v-model="client.apiUrl" v-if="isLocalApiClient(client)" label="API URL"></v-text-field>
<v-select v-model="client.model" v-if="client.type === 'openai'" :items="['gpt-4-1106-preview', 'gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k']" label="Model"></v-select>
<v-row>
<v-col :cols="clientMeta().enable_api_auth ? 7 : 12">
<v-text-field v-model="client.api_url" v-if="requiresAPIUrl(client)" label="API URL"></v-text-field>
</v-col>
<v-col cols="5">
<v-text-field type="password" v-model="client.api_key" v-if="requiresAPIUrl(client) && clientMeta().enable_api_auth" label="API Key"></v-text-field>
</v-col>
</v-row>
<v-select v-model="client.model" v-if="clientMeta().manual_model && clientMeta().manual_model_choices" :items="clientMeta().manual_model_choices" label="Model"></v-select>
<v-text-field v-model="client.model_name" v-else-if="clientMeta().manual_model" label="Manually specify model name" hint="It looks like we're unable to retrieve the model name automatically. The model name is used to match the appropriate prompt template. This is likely only important if you're locally serving a model."></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="4">
<v-text-field v-model="client.max_token_length" v-if="isLocalApiClient(client)" type="number" label="Context Length"></v-text-field>
<v-text-field v-model="client.max_token_length" v-if="requiresAPIUrl(client)" type="number" label="Context Length"></v-text-field>
</v-col>
<v-col cols="8" v-if="!typeEditable() && client.data && client.data.prompt_template_example !== null">
<v-col cols="8" v-if="!typeEditable() && client.data && client.data.prompt_template_example !== null && client.model_name && clientMeta().requires_prompt_template">
<v-combobox ref="promptTemplateComboBox" label="Prompt Template" v-model="client.data.template_file" @update:model-value="setPromptTemplate" :items="promptTemplates"></v-combobox>
<v-card elevation="3" :color="(client.data.has_prompt_template ? 'primary' : 'warning')" variant="tonal">
<v-card-title>Prompt Template</v-card-title>
<v-card-text>
<div class="text-caption" v-if="!client.data.has_prompt_template">No matching LLM prompt template found. Using default.</div>
<pre>{{ client.data.prompt_template_example }}</pre>
</v-card-text>
<v-card-actions>
<v-btn @click.stop="determineBestTemplate" prepend-icon="mdi-web-box">Determine via HuggingFace</v-btn>
</v-card-actions>
</v-card>
</v-col>
@@ -55,30 +70,19 @@ export default {
dialog: Boolean,
formTitle: String
},
inject: ['state'],
inject: [
'state',
'getWebsocket',
'registerMessageHandler',
],
data() {
return {
promptTemplates: [],
clientTypes: [],
clientChoices: [],
localDialog: this.state.dialog,
client: { ...this.state.currentClient },
defaultValuesByCLientType: {
// when client type is changed in the modal, these values will be used
// to populate the form
'textgenwebui': {
apiUrl: 'http://localhost:5000',
max_token_length: 4096,
name_prefix: 'TextGenWebUI',
},
'openai': {
model: 'gpt-4-1106-preview',
name_prefix: 'OpenAI',
max_token_length: 16384,
},
'lmstudio': {
apiUrl: 'http://localhost:1234',
max_token_length: 4096,
name_prefix: 'LMStudio',
}
}
defaultValuesByCLientType: {}
};
},
watch: {
@@ -86,6 +90,10 @@ export default {
immediate: true,
handler(newVal) {
this.localDialog = newVal;
if (newVal) {
this.requestClientTypes();
this.requestStdTemplates();
}
}
},
'state.currentClient': {
@@ -103,13 +111,13 @@ export default {
const defaults = this.defaultValuesByCLientType[this.client.type];
if (defaults) {
this.client.model = defaults.model || '';
this.client.apiUrl = defaults.apiUrl || '';
this.client.api_url = defaults.api_url || '';
this.client.max_token_length = defaults.max_token_length || 4096;
// loop and build name from prefix, checking against current clients
let name = defaults.name_prefix;
let name = this.clientTypes[this.client.type].name_prefix;
let i = 2;
while (this.state.clients.find(c => c.name === name)) {
name = `${defaults.name_prefix} ${i}`;
name = `${name} ${i}`;
i++;
}
this.client.name = name;
@@ -141,12 +149,90 @@ export default {
return;
}
if(this.clientMeta().manual_model && !this.clientMeta().manual_model_choices) {
this.client.model = this.client.model_name;
}
this.$emit('save', this.client); // Emit save event with client object
this.close();
},
isLocalApiClient(client) {
return client.type === 'textgenwebui' || client.type === 'lmstudio';
clientMeta() {
if(!Object.keys(this.clientTypes).length)
return {defaults:{}};
if(!this.clientTypes[this.client.type])
return {defaults:{}};
return this.clientTypes[this.client.type];
},
requiresAPIUrl() {
return this.clientMeta().defaults.api_url != null;
},
requestStdTemplates() {
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'request_std_llm_templates',
data: {}
}));
},
requestClientTypes() {
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'request_client_types',
data: {}
}));
},
determineBestTemplate() {
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'determine_llm_template',
data: {
model: this.client.model_name,
}
}));
},
setPromptTemplate() {
this.getWebsocket().send(JSON.stringify({
type: 'config',
action: 'set_llm_template',
data: {
template_file: this.client.data.template_file,
model: this.client.model_name,
}
}));
this.$refs.promptTemplateComboBox.blur();
},
handleMessage(data) {
if (data.type === 'config' && data.action === 'set_llm_template_complete') {
this.client.data.has_prompt_template = data.data.has_prompt_template;
this.client.data.prompt_template_example = data.data.prompt_template_example;
this.client.data.template_file = data.data.template_file;
} else if (data.type === 'config' && data.action === 'std_llm_templates') {
console.log("Got std templates", data.data.templates);
this.promptTemplates = data.data.templates;
} else if (data.type === 'config' && data.action === 'client_types') {
console.log("Got client types", data.data);
this.clientTypes = data.data;
// build clientChoices from clientTypes
// build defaults from clientTypes[type].defaults
this.clientChoices = [];
for (let client_type in this.clientTypes) {
this.clientChoices.push({
title: this.clientTypes[client_type].title,
value: client_type,
});
this.defaultValuesByCLientType[client_type] = this.clientTypes[client_type].defaults;
}
}
}
}
},
created() {
this.registerMessageHandler(this.handleMessage);
},
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<CreativeMenu ref="menu"/>
<CreativeMenu ref="menu" @open-world-state-manager="onOpenWorldStateManager"/>
<CharacterCreator ref="characterCreator"/>
<CharacterImporter ref="characterImporter"/>
<SceneCreator ref="sceneCreator"/>
@@ -40,7 +40,14 @@ export default {
'setWaitingForInput',
],
emits: [
'open-world-state-manager',
],
methods: {
onOpenWorldStateManager(tab, sub1, sub2, sub3) {
this.$emit('open-world-state-manager', tab, sub1, sub2, sub3);
},
handleMessage(data) {
if(data.type === 'world_state') {
console.log("world_state");

View File

@@ -5,35 +5,77 @@
</v-list-subheader>
<div ref="charactersContainer">
<v-list>
<v-list density="compact">
<!-- active characters -->
<v-list-item density="compact" v-for="(character,index) in scene.characters" :key="index">
<v-list-item-title>
{{ character.name }}
</v-list-item-title>
<div class="text-center mt-1 mb-1">
<v-tooltip text="Remove">
<v-tooltip text="Permanently Delete">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" color="red" icon="mdi-account-cancel" @click.stop="removeCharacterFromScene(character.name)"></v-btn>
</template>
</v-tooltip>
<v-tooltip text="Edit character">
<v-tooltip text="Deactivate">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-edit" @click.stop="openCharacterCreatorForCharacter(character.name)"></v-btn>
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" color="secondary" icon="mdi-exit-run" @click.stop="getWebsocket().send(JSON.stringify({type: 'interact', text: '!char_d:'+character.name+':no'}))"></v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="false" text="Character sheet">
<v-tooltip text="Edit character">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-details"></v-btn>
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-edit" @click.stop="openWorldStateManager('characters',character.name, 'description')"></v-btn>
</template>
</v-tooltip>
<v-tooltip text="Open character template in character creator" v-if="character.base_attributes._template">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-badge-account-outline" @click.stop="openCharacterCreatorForCharacter(character.name)"></v-btn>
</template>
</v-tooltip>
</div>
<v-divider></v-divider>
</v-list-item>
<!-- inactive characters -->
<v-list-item v-for="(character_name, index) in scene.inactive_characters" density="compact" :key="index">
<v-list-item-title class="text-grey-darken-1">
{{ character_name }}
</v-list-item-title>
<div class="text-center mt-1 mb-1">
<v-tooltip text="Permanently Delete">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" color="red" icon="mdi-account-cancel" @click.stop="removeCharacterFromScene(character_name)"></v-btn>
</template>
</v-tooltip>
<v-tooltip text="Activate (call to scene)">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" color="secondary" icon="mdi-human-greeting" @click.stop="getWebsocket().send(JSON.stringify({type: 'interact', text: '!char_a:'+character_name+':no'}))"></v-btn>
</template>
</v-tooltip>
<v-tooltip text="Edit character">
<template v-slot:activator="{ props }">
<v-btn size="x-small" class="mr-1" v-bind="props" variant="tonal" density="comfortable" rounded="sm" icon="mdi-account-edit" @click.stop="openWorldStateManager('characters',character_name, 'description')"></v-btn>
</template>
</v-tooltip>
</div>
<v-divider></v-divider>
</v-list-item>
<!-- add / import character -->
<v-list-item>
<v-tooltip text="Add character">
<template v-slot:activator="{ props }">
@@ -92,6 +134,9 @@ export default {
'openCharacterImporter',
'openSceneCreator',
],
emits: [
'open-world-state-manager',
],
methods: {
toggle() {
this.expanded = !this.expanded;
@@ -99,7 +144,7 @@ export default {
removeCharacterFromScene(character) {
let confirm = window.confirm(`Are you sure you want to remove ${character} from the scene?`);
let confirm = window.confirm(`Are you sure you want to remove ${character} from the game?`);
if(!confirm) {
return;
@@ -111,6 +156,10 @@ export default {
}));
},
openWorldStateManager(tab, sub1, sub2, sub3) {
this.$emit('open-world-state-manager', tab, sub1, sub2, sub3);
},
handleMessage(data) {
if(data.type === 'scene_status' && data.status === 'started') {
this.scene = data.data;

View File

@@ -0,0 +1,172 @@
<template>
<v-card flat variant="text" v-if="hasRecentScenes()">
<v-card-title>
Recent Saves
</v-card-title>
<v-card-subtitle>
Continue your story
</v-card-subtitle>
<!--
horizontal scroll from config.recent_scenes.scenes
if sceneLoadingAvailable, clicking the scene should load it
scene object has the following properties:
- name
- path (path to load)
- filename (filename to display, sans extension)
- cover_image (cover image to request - asset id)
- date (date to display, iso format)
-->
<v-card-text v-if="config != null">
<div class="tiles">
<div class="tile" v-for="(scene, index) in recentScenes()" :key="index">
<v-card density="compact" elevation="7" @click="loadScene(scene)" color="primary" variant="outlined">
<v-card-title>
{{ scene.name }}
</v-card-title>
<v-card-subtitle>
{{ scene.filename }}
</v-card-subtitle>
<v-card-text>
<div class="cover-image-placeholder">
<v-img cover v-if="scene.cover_image != null && coverImages[scene.cover_image.id] != null" :src="getCoverImageSrc(scene.cover_image.id)"></v-img>
</div>
<p class="text-caption text-center text-grey-lighten-1">{{ prettyDate(scene.date) }}</p>
</v-card-text>
</v-card>
</div>
</div>
</v-card-text>
</v-card>
</template>
<script>
export default {
name: 'IntroRecentScenes',
props: {
sceneLoadingAvailable: Boolean,
config: Object,
},
inject: ['requestAssets', 'getWebsocket', 'registerMessageHandler'],
data() {
return {
coverImages: {},
}
},
emits: ['request-scene-load'],
watch: {
config(newVal) {
if(newVal != null) {
this.requestCoverImages();
}
}
},
methods: {
hasRecentScenes() {
return this.config != null && this.config.recent_scenes != null && this.config.recent_scenes.scenes != null && this.config.recent_scenes.scenes.length > 0;
},
prettyDate(date) {
// 2024-01-20T03:35:00.109492
let d = new Date(date);
return d.toLocaleString();
},
requestCoverImages() {
if(this.config.recent_scenes != null) {
if(this.config.recent_scenes.scenes != null) {
let coverImageIds = [];
for(let scene of this.config.recent_scenes.scenes) {
if(scene.cover_image != null) {
coverImageIds.push({
"path": scene.path,
"id": scene.cover_image.id,
"media_type": scene.cover_image.media_type,
"file_type": scene.cover_image.file_type,
});
}
}
this.requestAssets(coverImageIds);
}
}
},
loadScene(scene) {
this.$emit("request-scene-load", scene.path)
},
recentScenes() {
if(!this.config.recent_scenes) {
return [];
}
return this.config.recent_scenes.scenes;
},
getCachedCoverImage(assetId) {
if(this.coverImages[assetId]) {
return this.coverImages[assetId];
} else {
return null;
}
},
getCoverImageSrc(assetId) {
if(this.coverImages[assetId]) {
return 'data:'+this.coverImages[assetId].mediaType+';base64, '+this.coverImages[assetId].base64;
} else {
return null;
}
},
handleMessage(data) {
if(data.type === 'assets') {
console.log("ASSEsTS", data.assets)
for(let id in data.assets) {
let asset = data.assets[id];
this.coverImages[id] = {
base64: asset.base64,
mediaType: asset.mediaType,
};
}
console.log("assets", this.coverImages, data)
}
},
},
created() {
this.registerMessageHandler(this.handleMessage);
},
}
</script>
<style scoped>
.cover-image-placeholder {
position: relative;
height: 275px;
width: 100%;
background-color: transparent;
background-image: url('/src/assets/logo-13.1-backdrop.png');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
overflow: hidden;
}
/* flud flex tiles with fixed width */
.tiles {
display: flex;
flex-wrap: wrap;
justify-content: left;
overflow: hidden;
}
.tile {
flex: 0 0 275px;
margin: 10px;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<v-row>
<v-col cols="12" v-if="sceneLoadingAvailable">
<IntroRecentScenes :config="config" :scene-loading-available="sceneLoadingAvailable" @request-scene-load="requestSceneLoad"/>
</v-col>
</v-row>
<v-row v-if="false">
<v-col cols="4">
<!-- Welcome / Recent changes / Version INFO-->
<v-card>
<v-card-title>
What's new?
</v-card-title>
<v-card-subtitle>version {{ version }}</v-card-subtitle>
<v-card-text>
<v-list dense>
<v-list-item v-for="(item, index) in changelog" :key="index">
{{ item }}
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script>
import IntroRecentScenes from './IntroRecentScenes.vue';
export default {
name: 'IntroView',
components: {
IntroRecentScenes
},
props: {
version: String,
sceneLoadingAvailable: Boolean,
config: Object,
},
emits: ['request-scene-load'],
data() {
return {
changelog: [
"This screen was added",
"Item 2",
"Item 3",
"Item 4",
]
}
},
methods: {
requestSceneLoad(scene) {
this.$emit('request-scene-load', scene);
}
}
}
</script>

View File

@@ -165,6 +165,13 @@ export default {
this.sceneInput = '';
}
},
loadJsonSceneFromPath(path) {
this.loading = true;
this.$emit("loading", true)
this.getWebsocket().send(JSON.stringify({ type: 'load_scene', file_path: path }));
},
handleMessage(data) {
// Handle app configuration
if (data.type === 'app_config') {

View File

@@ -1,6 +1,10 @@
<template>
<v-alert variant="text" :closable="message_id !== null" type="info" icon="mdi-script-text-outline" elevation="0" density="compact" @click:close="deleteMessage()" @mouseover="hovered=true" @mouseleave="hovered=false">
<v-alert variant="text" type="info" icon="mdi-script-text-outline" elevation="0" density="compact" @mouseover="hovered=true" @mouseleave="hovered=false">
<template v-slot:close>
<v-btn size="x-small" icon @click="deleteMessage">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
<div class="narrator-message">
<v-textarea ref="textarea" v-if="editing" v-model="editing_text" @keydown.enter.prevent="submitEdit()" @blur="cancelEdit()" @keydown.escape.prevent="cancelEdit()">
</v-textarea>

View File

@@ -144,13 +144,13 @@ export default {
// find message where type == "character" and id == data.id
// remove that message from the array
let newMessages = [];
for (i = 0; i < this.messages.length; i++) {
if (this.messages[i].id == data.id) {
this.messages.splice(i, 1);
break;
if (this.messages[i].id != data.id) {
newMessages.push(this.messages[i]);
}
}
this.messages = newMessages;
return
}

View File

@@ -4,12 +4,17 @@
<v-spacer></v-spacer>
<!-- quick settings as v-chips -->
<v-chip size="x-small" v-for="(option, index) in quickSettings" :key="index" @click="toggleQuickSetting(option.value)"
:color="option.status() ? 'success' : 'grey'"
:color="option.status() === true ? 'success' : 'grey'"
:disabled="isInputDisabled()" class="ma-1">
<v-icon class="mr-1">{{ option.icon }}</v-icon>
{{ option.title }}
<v-icon class="ml-1" v-if="option.status()">mdi-check-circle-outline</v-icon>
<v-icon class="ml-1" v-else>mdi-circle-outline</v-icon>
<v-icon class="ml-1" v-if="option.status() === true">mdi-check-circle-outline</v-icon>
<v-icon class="ml-1" v-else-if="option.status() === false">mdi-circle-outline</v-icon>
<v-tooltip v-else :text="option.status()">
<template v-slot:activator="{ props }">
<v-icon class="ml-1" v-bind="props" color="orange">mdi-alert-outline</v-icon>
</template>
</v-tooltip>
</v-chip>
</v-sheet>
@@ -30,20 +35,24 @@
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top"
text="Redo most recent AI message">
:text="'Redo most recent AI message.\n[Ctrl: Provide instructions, +Alt: Rewrite]'"
class="pre-wrap"
max-width="300px">
<template v-slot:activator="{ props }">
<v-btn class="hotkey" v-bind="props" :disabled="isInputDisabled()"
@click="sendHotButtonMessage('!rerun')" color="primary" icon>
@click="rerun" color="primary" icon>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top"
text="Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition)">
:text="'Redo most recent AI message (Nuke Option - use this to attempt to break out of repetition) \n[Ctrl: Provide instructions, +Alt: Rewrite]'"
class="pre-wrap"
max-width="300px">
<template v-slot:activator="{ props }">
<v-btn class="hotkey" v-bind="props" :disabled="isInputDisabled()"
@click="sendHotButtonMessage('!rerun:0.5')" color="primary" icon>
@click="rerunNuke" color="primary" icon>
<v-icon>mdi-nuke</v-icon>
</v-btn>
</template>
@@ -221,6 +230,68 @@
</v-list>
</v-menu>
<!-- creative / game mode toggle -->
<v-menu v-if="isEnvironment('scene')">
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-3" v-bind="props" :disabled="isInputDisabled()" color="primary" icon>
<v-icon>mdi-puzzle-edit</v-icon>
<v-icon v-if="potentialNewCharactersExist()" class="btn-notification" color="warning">mdi-human-greeting</v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader>Creative Tools</v-list-subheader>
<!-- deactivate active characters -->
<v-list-item v-for="(character, index) in deactivatableCharacters" :key="index"
@click="deactivateCharacter($event, character)">
<template v-slot:prepend>
<v-icon color="secondary">mdi-exit-run</v-icon>
</template>
<v-list-item-title>Take out of scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} a passive character.</v-list-item-subtitle>
</v-list-item>
<!-- reactivate inactive characters -->
<v-list-item v-for="(character, index) in inactiveCharacters" :key="index"
@click="activateCharacter($event, character)">
<template v-slot:prepend>
<v-icon color="secondary">mdi-human-greeting</v-icon>
</template>
<v-list-item-title>Call into scene: {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
</v-list-item>
<!-- persist passive characters -->
<v-list-item v-for="(character, index) in potentialNewCharacters()" :key="index"
@click="introduceCharacter($event, character)">
<template v-slot:prepend>
<v-icon color="warning">mdi-human-greeting</v-icon>
</template>
<v-list-item-title>Introduce {{ character }}<v-chip variant="text" color="info" class="ml-1" size="x-small">Ctrl: no narration</v-chip></v-list-item-title>
<v-list-item-subtitle>Make {{ character }} an active character.</v-list-item-subtitle>
</v-list-item>
<!-- static tools -->
<v-list-item v-for="(option, index) in creativeGameMenu" :key="index"
@click="sendHotButtonMessage('!' + option.value)"
:prepend-icon="option.icon">
<v-list-item-title>{{ option.title }}</v-list-item-title>
<v-list-item-subtitle>{{ option.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-menu>
<v-tooltip v-else-if="isEnvironment('creative')" :disabled="isInputDisabled()" location="top" text="Switch to game mode">
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-3" v-bind="props" :disabled="isInputDisabled()"
@click="sendHotButtonMessage('!setenv_scene')" color="primary" icon>
<v-icon>mdi-gamepad-square</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- save menu -->
<v-menu>
@@ -240,26 +311,6 @@
</v-list>
</v-menu>
<!-- creative / game mode toggle -->
<v-tooltip v-if="isEnvironment('scene')" :disabled="isInputDisabled()" location="top" text="Switch to creative mode">
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-3" v-bind="props" :disabled="isInputDisabled()"
@click="sendHotButtonMessage('!setenv_creative')" color="primary" icon>
<v-icon>mdi-palette-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip v-else-if="isEnvironment('creative')" :disabled="isInputDisabled()" location="top" text="Switch to game mode">
<template v-slot:activator="{ props }">
<v-btn class="hotkey mx-3" v-bind="props" :disabled="isInputDisabled()"
@click="sendHotButtonMessage('!setenv_scene')" color="primary" icon>
<v-icon>mdi-gamepad-square</v-icon>
</v-btn>
</template>
</v-tooltip>
</v-card-actions>
</v-card>
@@ -273,16 +324,35 @@
export default {
name: 'SceneTools',
props: {
passiveCharacters: Array,
inactiveCharacters: Array,
activeCharacters: Array,
playerCharacterName: String,
},
computed: {
deactivatableCharacters: function() {
// this.activeCharacters without playerCharacterName
let characters = [];
for (let character of this.activeCharacters) {
if (character !== this.playerCharacterName) {
characters.push(character);
}
}
return characters;
}
},
data() {
return {
commandActive: false,
commandName: null,
autoSave: true,
autoProgress: true,
canAutoSave: false,
npc_characters: [],
quickSettings: [
{"value": "toggleAutoSave", "title": "Auto Save", "icon": "mdi-content-save", "description": "Automatically save after each game-loop", "status": () => { return this.autoSave; }},
{"value": "toggleAutoSave", "title": "Auto Save", "icon": "mdi-content-save", "description": "Automatically save after each game-loop", "status": () => { return this.canAutoSave ? this.autoSave : "Manually save scene for auto-save to be available"; }},
{"value": "toggleAutoProgress", "title": "Auto Progress", "icon": "mdi-robot", "description": "AI automatically progresses after player turn.", "status": () => { return this.autoProgress }},
],
@@ -303,6 +373,11 @@ export default {
{"value": "ai_dialogue", "title": "Talk", "icon": "mdi-comment-text-outline", "description": "Generate dialogue"},
],
creativeGameMenu: [
{"value": "pc:prompt", "title": "Introduce new character (Directed)", "icon": "mdi-account-plus", "description": "Generate a new active character, based on prompt."},
{"value": "setenv_creative", "title": "Creative Mode", "icon": "mdi-puzzle-edit", "description": "Switch to creative mode (very early experimental version)"},
],
advanceTimeOptions: [
{"value" : "P10Y", "title": "10 years"},
{"value" : "P5Y", "title": "5 years"},
@@ -341,14 +416,63 @@ export default {
'getTrackedWorldState',
'getPlayerCharacterName',
'formatWorldStateTemplateString',
'characterSheet',
],
computed:{
},
emits: [
'open-world-state-manager',
],
methods: {
potentialNewCharacters() {
// return all entries in passiveCharacters that dont exist in
// inactiveCharacters
let newCharacters = [];
for (let character of this.passiveCharacters) {
if (!this.inactiveCharacters.includes(character)) {
newCharacters.push(character);
}
}
return newCharacters;
},
activateCharacter(ev, name) {
let modifyNoNarration = ev.ctrlKey;
if(!modifyNoNarration) {
this.sendHotButtonMessage('!char_a:' + name);
} else {
this.sendHotButtonMessage('!char_a:' + name + ':no');
}
},
deactivateCharacter(ev, name) {
let modifyNoNarration = ev.ctrlKey;
if(!modifyNoNarration) {
this.sendHotButtonMessage('!char_d:' + name);
} else {
this.sendHotButtonMessage('!char_d:' + name + ':no');
}
},
introduceCharacter(ev, name) {
let modifyNoNarration = ev.ctrlKey;
if(!modifyNoNarration) {
this.sendHotButtonMessage('!persist_character:' + name);
} else {
this.sendHotButtonMessage('!persist_character:' + name + ':no');
}
},
potentialNewCharactersExist() {
return this.potentialNewCharacters().length > 0;
},
passiveCharactersExist() {
return this.passiveCharacters.length > 0;
},
passiveCharacterExists(name) {
return this.passiveCharacters.includes(name);
},
isEnvironment(typ) {
return this.scene().environment == typ;
@@ -464,6 +588,38 @@ export default {
this.getWebsocket().send(JSON.stringify({ type: 'interact', text: '!ws' }));
},
rerun(event) {
console.log("EVENT", event)
// if ctrl is pressed use directed rerun
let withDirection = event.ctrlKey;
let method = event.altKey || event.metaKey ? "edit" : "replace";
let command = "!rerun";
if(withDirection)
command += "_directed";
command += ":0.0:"+method;
// if alt is pressed
this.sendHotButtonMessage(command)
},
rerunNuke(event) {
// if ctrl is pressed use directed rerun
let withDirection = event.ctrlKey;
let method = event.altKey || event.metaKey ? "edit" : "replace";
let command = "!rerun";
if(withDirection)
command += "_directed";
// 0.5 nuke adjustment
command += ":0.5:"+method;
this.sendHotButtonMessage(command)
},
handleMessage(data) {
if (data.type === "command_status") {
@@ -475,6 +631,7 @@ export default {
this.commandName = null;
}
} else if (data.type === "scene_status") {
this.canAutoSave = data.data.can_auto_save;
this.autoSave = data.data.auto_save;
this.autoProgress = data.data.auto_progress;
console.log({autoSave: this.autoSave, autoProgress: this.autoProgress});
@@ -492,7 +649,8 @@ export default {
}
}
},
},
mounted() {
console.log("Websocket", this.getWebsocket()); // Check if websocket is available
@@ -518,4 +676,21 @@ export default {
align-items: center;
margin-right: 20px;
}
.pre-wrap {
white-space: pre-wrap;
}
.btn-notification {
position: absolute;
top: 0px;
right: 0px;
font-size: 15px;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -17,10 +17,9 @@
<!-- <GameOptions v-if="sceneActive" ref="gameOptions" /> -->
<v-divider></v-divider>
<CoverImage v-if="sceneActive" ref="coverImage" />
<WorldState v-if="sceneActive" ref="worldState" />
<WorldState v-if="sceneActive" ref="worldState" @passive-characters="(characters) => { passiveCharacters = characters }" />
</div>
<CreativeEditor v-if="sceneActive" ref="creativeEditor" />
<CreativeEditor v-if="sceneActive" ref="creativeEditor" @open-world-state-manager="onOpenWorldStateManager" />
</v-list>
</v-navigation-drawer>
@@ -38,7 +37,7 @@
<v-list-subheader class="text-uppercase"><v-icon>mdi-network-outline</v-icon>
Clients</v-list-subheader>
<v-list-item>
<AIClient ref="aiClient" @save="saveClients" @error="uxErrorHandler" @clients-updated="saveClients" @client-assigned="saveAgents"></AIClient>
<AIClient ref="aiClient" @save="saveClients" @error="uxErrorHandler" @clients-updated="saveClients" @client-assigned="saveAgents" @open-app-config="openAppConfig"></AIClient>
</v-list-item>
<v-divider></v-divider>
<v-list-subheader class="text-uppercase"><v-icon>mdi-transit-connection-variant</v-icon> Agents</v-list-subheader>
@@ -120,7 +119,12 @@
<div style="flex-shrink: 0;" v-if="sceneActive">
<SceneTools @open-world-state-manager="onOpenWorldStateManager"/>
<SceneTools
@open-world-state-manager="onOpenWorldStateManager"
:playerCharacterName="getPlayerCharacterName()"
:passiveCharacters="passiveCharacters"
:inactiveCharacters="inactiveCharacters"
:activeCharacters="activeCharacters" />
<CharacterSheet ref="characterSheet" />
<SceneHistory ref="sceneHistory" />
@@ -141,6 +145,13 @@
</template>
</v-text-field>
</div>
<IntroView v-else
@request-scene-load="(path) => { $refs.loadScene.loadJsonSceneFromPath(path); }"
:version="version"
:scene-loading-available="!configurationRequired() && connected"
:config="appConfig" />
</v-container>
</v-main>
@@ -169,6 +180,8 @@ import DebugTools from './DebugTools.vue';
import AudioQueue from './AudioQueue.vue';
import StatusNotification from './StatusNotification.vue';
import IntroView from './IntroView.vue';
export default {
components: {
AIClient,
@@ -186,6 +199,7 @@ export default {
DebugTools,
AudioQueue,
StatusNotification,
IntroView,
},
name: 'TalemateApp',
data() {
@@ -209,6 +223,9 @@ export default {
inputHint: 'Enter your text...',
messageInput: '',
reconnectInterval: 3000,
passiveCharacters: [],
inactiveCharacters: [],
activeCharacters: [],
messageHandlers: [],
scene: {},
appConfig: {},
@@ -238,6 +255,7 @@ export default {
getClients: () => this.getClients(),
getAgents: () => this.getAgents(),
requestSceneAssets: (asset_ids) => this.requestSceneAssets(asset_ids),
requestAssets: (assets) => this.requestAssets(assets),
openCharacterSheet: (characterName) => this.openCharacterSheet(characterName),
characterSheet: () => this.$refs.characterSheet,
creativeEditor: () => this.$refs.creativeEditor,
@@ -305,6 +323,7 @@ export default {
if (data.id === 'scene.loaded') {
this.loading = false;
this.sceneActive = true;
this.requestAppConfig();
}
if(data.status == 'error') {
this.errorNotification = true;
@@ -325,6 +344,10 @@ export default {
player_character_name: data.data.player_character_name,
}
this.sceneActive = true;
this.inactiveCharacters = data.data.inactive_characters;
// data.data.characters is a list of all active characters in the scene
// collect character.name into list of active characters
this.activeCharacters = data.data.characters.map((character) => character.name);
return;
}
@@ -399,6 +422,9 @@ export default {
requestSceneAssets(asset_ids) {
this.websocket.send(JSON.stringify({ type: 'request_scene_assets', asset_ids: asset_ids }));
},
requestAssets(assets) {
this.websocket.send(JSON.stringify({ type: 'request_assets', assets: assets }));
},
setNavigation(navigation) {
if (navigation == "game")
this.sceneDrawer = true;
@@ -463,8 +489,8 @@ export default {
onOpenWorldStateManager(tab, sub1, sub2, sub3) {
this.$refs.worldState.openWorldStateManager(tab, sub1, sub2, sub3);
},
openAppConfig() {
this.$refs.appConfig.show();
openAppConfig(tab, page) {
this.$refs.appConfig.show(tab, page);
},
uxErrorHandler(error) {
this.errorNotification = true;
@@ -522,7 +548,7 @@ export default {
if (this.inputHint != this.scene.player_character_name+":") {
return 'warning';
} else {
return 'purple-lighten-3';
return 'deep-purple-lighten-2';
}
}
return null;

View File

@@ -228,6 +228,10 @@ export default {
'formatWorldStateTemplateString',
],
emits: [
'passive-characters',
],
methods: {
onResize() {
this.worldStateMaxHeight = this.availableHeight();
@@ -257,8 +261,18 @@ export default {
}
},
openWorldStateManager(tab, sub1, sub2, sub3) {
console.log("OPENING WORLDSTATE MANAGER", tab, sub1, sub2, sub3)
this.$refs.worldStateManager.show(tab, sub1, sub2, sub3);
},
passiveCharacters() {
let characters = [];
for(let character in this.characters) {
if(!this.characterSheet().characterExists(character)) {
characters.push(character);
}
}
this.$emit('passive-characters', characters);
},
lookAtCharacter(name) {
this.getWebsocket().send(JSON.stringify({
type: 'interact',
@@ -356,6 +370,8 @@ export default {
}
}
this.passiveCharacters();
//this.onResize()
} else if (data.type == "scene_status") {
this.sceneTime = data.data.scene_time;

View File

@@ -266,7 +266,7 @@
</v-col>
<v-col cols="8">
<div v-if="selectedCharacterStateReinforcer">
<v-textarea rows="3" auto-grow max-rows="15" :label="selectedCharacterStateReinforcer" v-model="characterDetails.reinforcements[selectedCharacterStateReinforcer].answer" @update:modelValue="queueUpdateCharacterStateReinforcement(selectedCharacterStateReinforcer)" :color="characterStateReinforcerDirty ? 'info' : ''"></v-textarea>
<v-textarea rows="5" auto-grow max-rows="15" :label="selectedCharacterStateReinforcer" v-model="characterDetails.reinforcements[selectedCharacterStateReinforcer].answer" @update:modelValue="queueUpdateCharacterStateReinforcement(selectedCharacterStateReinforcer)" :color="characterStateReinforcerDirty ? 'info' : ''"></v-textarea>
<v-row>
<v-col cols="6">
@@ -303,10 +303,20 @@
</v-btn>
</div>
</v-col>
<v-col cols="6" class="text-right">
<v-col cols="6" class="text-right flex">
<v-btn rounded="sm" prepend-icon="mdi-refresh" @click.stop="runCharacterStateReinforcement(selectedCharacterStateReinforcer)" color="primary" variant="text">
Refresh State
</v-btn>
<v-tooltip text="Removes all previously generated reinforcements for this state and then regenerates it">
<template v-slot:activator="{ props }">
<v-btn v-if="resetCharacterStateReinforcerConfirm === true" v-bind="props" rounded="sm" prepend-icon="mdi-backup-restore" @click.stop="runCharacterStateReinforcement(selectedCharacterStateReinforcer, true)" color="warning" variant="text">
Confirm Reset State
</v-btn>
<v-btn v-else v-bind="props" rounded="sm" prepend-icon="mdi-backup-restore" @click.stop="resetCharacterStateReinforcerConfirm=true" color="warning" variant="text">
Reset State
</v-btn>
</template>
</v-tooltip>
</v-col>
</v-row>
</div>
@@ -655,6 +665,7 @@ export default {
removeCharacterAttributeConfirm: false,
removeCharacterDetailConfirm: false,
removeCharacterStateReinforcerConfirm: false,
resetCharacterStateReinforcerConfirm: false,
characterAttributeSearch: null,
characterDetailSearch: null,
@@ -833,6 +844,7 @@ export default {
this.newCharacterDetailValue = null;
this.removeCharacterAttributeConfirm = false;
this.removeCharacterDetailConfirm = false;
this.resetCharacterStateReinforcerConfirm = false;
this.characterAttributeSearch = null;
this.characterDetailSearch = null;
this.newCharacterStateReinforcerInterval = 10;
@@ -1141,14 +1153,18 @@ export default {
this.selectedCharacterStateReinforcer = Object.keys(this.characterDetails.reinforcements)[0];
},
runCharacterStateReinforcement(name) {
runCharacterStateReinforcement(name, reset) {
this.isBusy = true;
this.getWebsocket().send(JSON.stringify({
type: 'world_state_manager',
action: 'run_character_detail_reinforcement',
name: this.selectedCharacter,
question: name,
reset: reset || false,
}));
this.resetCharacterStateReinforcerConfirm = false;
},
// character description

View File

@@ -216,6 +216,16 @@
</v-btn>
</span>
<v-spacer></v-spacer>
<v-tooltip text="Removes all previously generated reinforcements for this state and then regenerates it">
<template v-slot:activator="{ props }">
<v-btn v-if="resetStateReinforcerConfirm === true" v-bind="props" rounded="sm" prepend-icon="mdi-backup-restore" @click.stop="runStateReinforcement(true)" color="warning" variant="text">
Confirm Reset State
</v-btn>
<v-btn v-else v-bind="props" rounded="sm" prepend-icon="mdi-backup-restore" @click.stop="resetStateReinforcerConfirm=true" color="warning" variant="text">
Reset State
</v-btn>
</template>
</v-tooltip>
<v-btn rounded="sm" prepend-icon="mdi-refresh" @click.stop="runStateReinforcement()" color="primary" variant="text">
Refresh State
</v-btn>
@@ -251,6 +261,7 @@ export default {
saveEntryTimeout: null,
deleteConfirm: false,
deferedNavigation: null,
resetStateReinforcerConfirm: false,
dirty: false,
busy: false,
baseEntry: {
@@ -487,13 +498,16 @@ export default {
}));
},
runStateReinforcement() {
runStateReinforcement(reset) {
this.busy=true;
this.getWebsocket().send(JSON.stringify({
type: 'world_state_manager',
action: 'run_world_state_reinforcement',
question: this.state.question
question: this.state.question,
reset: reset || false,
}));
this.resetStateReinforcerConfirm = false;
},
requestWorld: function () {

View File

@@ -4,9 +4,17 @@ import 'vuetify/styles'
// Vuetify
import { createVuetify } from 'vuetify'
import colors from 'vuetify/util/colors'
export default createVuetify({
theme : {
defaultTheme: 'dark'
defaultTheme: 'dark',
themes: {
dark: {
colors: {
primary: colors.deepPurple.lighten2,
}
}
}
}
})

View File

@@ -0,0 +1,7 @@
{{ system_message }}
### Instruction:
{{ user_message }}
### Response:
{{ coercion_message }}

View File

@@ -0,0 +1,6 @@
<|im_start|>system
{{ system_message }}<|im_end|>
<|im_start|>user
{{ user_message }}<|im_end|>
<|im_start|>assistant
{{ coercion_message }}

View File

@@ -0,0 +1,8 @@
### Instruction:
{{ system_message }}
### Input:
{{ user_message }}
### Response:
{{ coercion_message }}

View File

@@ -0,0 +1 @@
<s>[INST] {{ system_message }} {{ user_message }} [/INST] {{ coercion_message }}

View File

@@ -0,0 +1 @@
GPT4 Correct System: {{ system_message }}<|end_of_turn|>GPT4 Correct User: {{ user_message }}<|end_of_turn|>GPT4 Correct Assistant: {{ coercion_message }}

View File

@@ -0,0 +1 @@
USER: {{ system_message }} {{ user_message }} ASSISTANT: {{ coercion_message }}

View File

@@ -0,0 +1,2 @@
User: {{ system_message }} {{ user_message }}
Assistant: {{ coercion_message }}

View File

@@ -0,0 +1,3 @@
SYSTEM: {{ system_message }}
USER: {{ user_message }}
ASSISTANT: {{ coercion_message }}

View File

@@ -0,0 +1,6 @@
<|system|>
{{ system_message }}</s>
<|user|>
{{ user_message }}</s>
<|assistant|>
{{ coercion_message }}

Some files were not shown because too many files have changed in this diff Show More